From 831e1d507251d488c8cb22d98dda69a158faf461 Mon Sep 17 00:00:00 2001 From: Jorge Rivera Date: Thu, 29 Oct 2026 23:43:04 +0100 Subject: [PATCH 1/7] feat(registry): register the export and MCP refusal codes Adds the datasets.exports and datasets.export_link endpoints for the dfo-apis export routes, with provenance_unavailable and licence_not_redistributable. Adds the mcp_refusal channel and mcp.tools endpoint for refusals an MCP tool result carries, with tool_not_found, frame_ref_incomplete, frame_not_held, frame_expired and rolling_reanchor_unavailable. --- .../src/dfo_client_contract/actions.json | 8 + python/spec-kit/CHANGELOG.md | 32 +++ .../src/dfo_spec_kit/registry/codes.json | 202 ++++++++++++++++++ registries/codes.json | 202 ++++++++++++++++++ schemas/registry/README.md | 7 +- schemas/registry/code-registry.schema.json | 17 +- tools/validate_registry.py | 2 + 7 files changed, 463 insertions(+), 7 deletions(-) diff --git a/python/client-contract/src/dfo_client_contract/actions.json b/python/client-contract/src/dfo_client_contract/actions.json index cb28717..98d6e4c 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 70e3735..b43085f 100644 --- a/python/spec-kit/CHANGELOG.md +++ b/python/spec-kit/CHANGELOG.md @@ -252,6 +252,38 @@ 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 an MCP tool result + carries, on endpoint `mcp.tools` and channel `mcp_refusal`: an unknown or + removed tool, a frame reference that lacks its content version, a frame + never stored, a frame aged out of the working tier, and a derived frame that + follows the latest data and has no stored computation tree to replay under a + shared period. `provenance_unavailable` (422) and + `licence_not_redistributable` (403) are on `datasets.exports`, the latter + also on `datasets.export_link`: an export of a derived frame with no stored + recipe, and an export whose source licence forbids redistribution. +- Endpoint `mcp.tools` and channel `mcp_refusal` in + `schemas/registry/code-registry.schema.json`, its README and + `tools/validate_registry.py`: the refusal envelope of an MCP tool result + (dfo-ai data-mcp, analysis-mcp, presentation-mcp). A mapping on it carries no + `http_status`, and `mcp_refusal` is a refusal-only channel. +- 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). `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), + `licence_not_redistributable` (403), `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 6ec8b03..dae55da 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,11 @@ "endpoint": "presentation.shares", "channel": "problem_details", "http_status": 429 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 429 } ], "lifecycle": "active" @@ -78,6 +83,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 410 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 410 } ], "lifecycle": "active" @@ -329,6 +339,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 501 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 501 } ], "lifecycle": "active" @@ -1438,6 +1453,11 @@ "endpoint": "datasets.files", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -1464,6 +1484,11 @@ "endpoint": "datasets.files", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -1740,6 +1765,36 @@ ], "lifecycle": "active" }, + { + "code": "frame_expired", + "classification": "refusal", + "definition": "The frame a reference names was stored and has since aged out of the working tier, so none of its artifacts remains servable.", + "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 was never stored here, or the stored identity and version do not belong together.", + "owning_contract": "mcp", + "owner": "platform", + "introduced_in": "v1", + "mappings": [ + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" + } + ], + "lifecycle": "active" + }, { "code": "frame_not_refreshable", "classification": "refusal", @@ -1756,6 +1811,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", @@ -1927,6 +1997,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 500 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 500 } ], "lifecycle": "active" @@ -2018,6 +2093,16 @@ "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 } ], "lifecycle": "active" @@ -2096,6 +2181,27 @@ ], "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 + } + ], + "lifecycle": "active" + }, { "code": "member_not_resolvable", "classification": "refusal", @@ -2488,6 +2594,11 @@ "endpoint": "presentation.shares", "channel": "problem_details", "http_status": 404 + }, + { + "endpoint": "datasets.export_link", + "channel": "problem_details", + "http_status": 404 } ], "lifecycle": "active" @@ -3058,6 +3169,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -3089,6 +3205,11 @@ "endpoint": "datasets.files", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -3347,6 +3468,22 @@ ], "lifecycle": "active" }, + { + "code": "provenance_unavailable", + "classification": "refusal", + "definition": "A derived frame has no stored recipe, so its processing steps and input versions cannot be listed for an export.", + "owning_contract": "datasets", + "owner": "platform", + "introduced_in": "v1", + "mappings": [ + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 + } + ], + "lifecycle": "active" + }, { "code": "provider_ambiguous", "classification": "refusal", @@ -3889,6 +4026,11 @@ "endpoint": "presentation.shares", "channel": "problem_details", "http_status": 400 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 400 } ], "lifecycle": "active" @@ -4275,6 +4417,21 @@ ], "lifecycle": "active" }, + { + "code": "rolling_reanchor_unavailable", + "classification": "refusal", + "definition": "A derived frame whose period follows the latest data cannot be computed again under the latest period shared with the other frames in the request, because its stored recipe has no computation tree to replay.", + "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 +4459,11 @@ "endpoint": "datasets.files", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -4515,6 +4677,16 @@ "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 } ], "lifecycle": "active" @@ -5060,6 +5232,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 404 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 404 } ], "lifecycle": "active" @@ -5126,6 +5303,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 410 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 410 } ], "lifecycle": "active" @@ -5364,6 +5546,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 404 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 404 } ], "lifecycle": "active" @@ -5597,6 +5784,21 @@ ], "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": "un_system_unavailable", "classification": "warning", diff --git a/registries/codes.json b/registries/codes.json index 6ec8b03..dae55da 100644 --- a/registries/codes.json +++ b/registries/codes.json @@ -23,6 +23,11 @@ "endpoint": "presentation.shares", "channel": "problem_details", "http_status": 429 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 429 } ], "lifecycle": "active" @@ -78,6 +83,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 410 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 410 } ], "lifecycle": "active" @@ -329,6 +339,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 501 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 501 } ], "lifecycle": "active" @@ -1438,6 +1453,11 @@ "endpoint": "datasets.files", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -1464,6 +1484,11 @@ "endpoint": "datasets.files", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -1740,6 +1765,36 @@ ], "lifecycle": "active" }, + { + "code": "frame_expired", + "classification": "refusal", + "definition": "The frame a reference names was stored and has since aged out of the working tier, so none of its artifacts remains servable.", + "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 was never stored here, or the stored identity and version do not belong together.", + "owning_contract": "mcp", + "owner": "platform", + "introduced_in": "v1", + "mappings": [ + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" + } + ], + "lifecycle": "active" + }, { "code": "frame_not_refreshable", "classification": "refusal", @@ -1756,6 +1811,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", @@ -1927,6 +1997,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 500 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 500 } ], "lifecycle": "active" @@ -2018,6 +2093,16 @@ "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 } ], "lifecycle": "active" @@ -2096,6 +2181,27 @@ ], "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 + } + ], + "lifecycle": "active" + }, { "code": "member_not_resolvable", "classification": "refusal", @@ -2488,6 +2594,11 @@ "endpoint": "presentation.shares", "channel": "problem_details", "http_status": 404 + }, + { + "endpoint": "datasets.export_link", + "channel": "problem_details", + "http_status": 404 } ], "lifecycle": "active" @@ -3058,6 +3169,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -3089,6 +3205,11 @@ "endpoint": "datasets.files", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -3347,6 +3468,22 @@ ], "lifecycle": "active" }, + { + "code": "provenance_unavailable", + "classification": "refusal", + "definition": "A derived frame has no stored recipe, so its processing steps and input versions cannot be listed for an export.", + "owning_contract": "datasets", + "owner": "platform", + "introduced_in": "v1", + "mappings": [ + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 + } + ], + "lifecycle": "active" + }, { "code": "provider_ambiguous", "classification": "refusal", @@ -3889,6 +4026,11 @@ "endpoint": "presentation.shares", "channel": "problem_details", "http_status": 400 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 400 } ], "lifecycle": "active" @@ -4275,6 +4417,21 @@ ], "lifecycle": "active" }, + { + "code": "rolling_reanchor_unavailable", + "classification": "refusal", + "definition": "A derived frame whose period follows the latest data cannot be computed again under the latest period shared with the other frames in the request, because its stored recipe has no computation tree to replay.", + "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 +4459,11 @@ "endpoint": "datasets.files", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -4515,6 +4677,16 @@ "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 } ], "lifecycle": "active" @@ -5060,6 +5232,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 404 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 404 } ], "lifecycle": "active" @@ -5126,6 +5303,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 410 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 410 } ], "lifecycle": "active" @@ -5364,6 +5546,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 404 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 404 } ], "lifecycle": "active" @@ -5597,6 +5784,21 @@ ], "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": "un_system_unavailable", "classification": "warning", diff --git a/schemas/registry/README.md b/schemas/registry/README.md index df4da75..f097c3a 100644 --- a/schemas/registry/README.md +++ b/schemas/registry/README.md @@ -36,7 +36,7 @@ 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. @@ -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,6 +67,8 @@ 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`, a refusal an MCP tool result carries (dfo-ai data-mcp, analysis-mcp, presentation-mcp). It uses the `mcp_refusal` channel alone, so its mappings carry no `http_status`, and a code on it is not returned by any HTTP route. + 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. @@ -86,6 +88,7 @@ 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 an MCP tool result carries: a `code`, a `retry_mode`, a message and a `fix`. It has no HTTP status. `http_status` is present only when `channel` is `problem_details`. diff --git a/schemas/registry/code-registry.schema.json b/schemas/registry/code-registry.schema.json index 23765d4..c05ef90 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" @@ -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 an MCP tool result carries, a `code` with a `retry_mode`, a message and a `fix`, 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": { @@ -261,6 +265,8 @@ "datasets.refresh", "datasets.operations", "datasets.analysis", + "datasets.exports", + "datasets.export_link", "graph.node", "graph.nodes", "graph.members", @@ -272,7 +278,8 @@ "platform.codes", "skills.list", "skills.fetch", - "skills.resource" + "skills.resource", + "mcp.tools" ] } }, 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", From a5889769523b79866ecbf65322afb8a50d126231 Mon Sep 17 00:00:00 2001 From: Jorge Rivera Date: Fri, 30 Oct 2026 00:12:33 +0100 Subject: [PATCH 2/7] feat(registry): refuse a revoked version on the export link --- python/spec-kit/CHANGELOG.md | 4 ++-- python/spec-kit/src/dfo_spec_kit/registry/codes.json | 5 +++++ registries/codes.json | 5 +++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/python/spec-kit/CHANGELOG.md b/python/spec-kit/CHANGELOG.md index b43085f..bdc84ef 100644 --- a/python/spec-kit/CHANGELOG.md +++ b/python/spec-kit/CHANGELOG.md @@ -282,8 +282,8 @@ and this package adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `rollup_not_summable` (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), - `licence_not_redistributable` (403), `service_unavailable` (503) and - `internal_error` (500). + `snapshot_revoked` (410), `licence_not_redistributable` (403), + `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 dae55da..5c51b32 100644 --- a/python/spec-kit/src/dfo_spec_kit/registry/codes.json +++ b/python/spec-kit/src/dfo_spec_kit/registry/codes.json @@ -5308,6 +5308,11 @@ "endpoint": "datasets.exports", "channel": "problem_details", "http_status": 410 + }, + { + "endpoint": "datasets.export_link", + "channel": "problem_details", + "http_status": 410 } ], "lifecycle": "active" diff --git a/registries/codes.json b/registries/codes.json index dae55da..5c51b32 100644 --- a/registries/codes.json +++ b/registries/codes.json @@ -5308,6 +5308,11 @@ "endpoint": "datasets.exports", "channel": "problem_details", "http_status": 410 + }, + { + "endpoint": "datasets.export_link", + "channel": "problem_details", + "http_status": 410 } ], "lifecycle": "active" From 3c5c7fdbd428f4bc8597bc84d4c3f9a97eb03951 Mon Sep 17 00:00:00 2001 From: Jorge Rivera Date: Fri, 30 Oct 2026 01:36:53 +0100 Subject: [PATCH 3/7] feat(registry): register the rebuild refusals on datasets.exports A prepare that names only a spec_id resolves its newest version and can rebuild it, so datasets.exports carries the 21 codes datasets.definition answers for a rebuild, at the same statuses. --- python/spec-kit/CHANGELOG.md | 13 ++- .../src/dfo_spec_kit/registry/codes.json | 105 ++++++++++++++++++ registries/codes.json | 105 ++++++++++++++++++ 3 files changed, 222 insertions(+), 1 deletion(-) diff --git a/python/spec-kit/CHANGELOG.md b/python/spec-kit/CHANGELOG.md index 534463e..acdd128 100644 --- a/python/spec-kit/CHANGELOG.md +++ b/python/spec-kit/CHANGELOG.md @@ -287,7 +287,18 @@ and this package adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `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). `datasets.export_link` is + `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 `datasets.definition` answers for a rebuild, 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`, `entities_unbound`, `excluded_by_registry`, + `facet_binding_unsupported`, `facet_not_available`, `invalid_role_ref`, + `member_not_resolvable`, `rate_coverage_gap`, `recipe_not_declared`, + `resolution_ambiguous`, `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), 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 0f4da5c..ba4d660 100644 --- a/python/spec-kit/src/dfo_spec_kit/registry/codes.json +++ b/python/spec-kit/src/dfo_spec_kit/registry/codes.json @@ -490,6 +490,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -532,6 +537,11 @@ "endpoint": "datasets.definition", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -874,6 +884,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -930,6 +945,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -1282,6 +1302,11 @@ "endpoint": "datasets.options", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -1525,6 +1550,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -1585,6 +1615,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -1693,6 +1728,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -1926,6 +1966,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 500 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 500 } ], "lifecycle": "active" @@ -2145,6 +2190,11 @@ "endpoint": "datasets.options", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -2239,6 +2289,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -2312,6 +2367,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 500 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 500 } ], "lifecycle": "active" @@ -3698,6 +3758,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -3758,6 +3823,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -4084,6 +4154,11 @@ "endpoint": "datasets.options", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -4242,6 +4317,11 @@ "endpoint": "datasets.options", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -4347,6 +4427,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -5517,6 +5602,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 409 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 409 } ], "lifecycle": "active" @@ -5817,6 +5907,11 @@ "endpoint": "presentation.shares", "channel": "problem_details", "http_status": 504 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 504 } ], "lifecycle": "active" @@ -6108,6 +6203,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 502 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 502 } ], "lifecycle": "active" @@ -6154,6 +6254,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 503 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 503 } ], "lifecycle": "active" diff --git a/registries/codes.json b/registries/codes.json index 0f4da5c..ba4d660 100644 --- a/registries/codes.json +++ b/registries/codes.json @@ -490,6 +490,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -532,6 +537,11 @@ "endpoint": "datasets.definition", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -874,6 +884,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -930,6 +945,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -1282,6 +1302,11 @@ "endpoint": "datasets.options", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -1525,6 +1550,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -1585,6 +1615,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -1693,6 +1728,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -1926,6 +1966,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 500 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 500 } ], "lifecycle": "active" @@ -2145,6 +2190,11 @@ "endpoint": "datasets.options", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -2239,6 +2289,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -2312,6 +2367,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 500 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 500 } ], "lifecycle": "active" @@ -3698,6 +3758,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -3758,6 +3823,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -4084,6 +4154,11 @@ "endpoint": "datasets.options", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -4242,6 +4317,11 @@ "endpoint": "datasets.options", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -4347,6 +4427,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -5517,6 +5602,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 409 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 409 } ], "lifecycle": "active" @@ -5817,6 +5907,11 @@ "endpoint": "presentation.shares", "channel": "problem_details", "http_status": 504 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 504 } ], "lifecycle": "active" @@ -6108,6 +6203,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 502 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 502 } ], "lifecycle": "active" @@ -6154,6 +6254,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 503 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 503 } ], "lifecycle": "active" From 0041a4aa3bf5a9990dd5b7c83f8c65981b54fd37 Mon Sep 17 00:00:00 2001 From: Jorge Rivera Date: Fri, 30 Oct 2026 01:40:55 +0100 Subject: [PATCH 4/7] fix(registry): keep only the rebuild refusals dfo-apis serves on datasets.exports entities_unbound and resolution_ambiguous are catalogue entries no rebuild raises. --- python/spec-kit/CHANGELOG.md | 6 +++--- python/spec-kit/src/dfo_spec_kit/registry/codes.json | 10 ---------- registries/codes.json | 10 ---------- 3 files changed, 3 insertions(+), 23 deletions(-) diff --git a/python/spec-kit/CHANGELOG.md b/python/spec-kit/CHANGELOG.md index acdd128..4995721 100644 --- a/python/spec-kit/CHANGELOG.md +++ b/python/spec-kit/CHANGELOG.md @@ -289,15 +289,15 @@ and this package adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `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 `datasets.definition` answers for a rebuild, at the same + 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`, `entities_unbound`, `excluded_by_registry`, + `definition_not_reproducible`, `excluded_by_registry`, `facet_binding_unsupported`, `facet_not_available`, `invalid_role_ref`, `member_not_resolvable`, `rate_coverage_gap`, `recipe_not_declared`, - `resolution_ambiguous`, `role_incompatible` and + `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), 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 ba4d660..2769ba8 100644 --- a/python/spec-kit/src/dfo_spec_kit/registry/codes.json +++ b/python/spec-kit/src/dfo_spec_kit/registry/codes.json @@ -1302,11 +1302,6 @@ "endpoint": "datasets.options", "channel": "problem_details", "http_status": 422 - }, - { - "endpoint": "datasets.exports", - "channel": "problem_details", - "http_status": 422 } ], "lifecycle": "active" @@ -4154,11 +4149,6 @@ "endpoint": "datasets.options", "channel": "problem_details", "http_status": 422 - }, - { - "endpoint": "datasets.exports", - "channel": "problem_details", - "http_status": 422 } ], "lifecycle": "active" diff --git a/registries/codes.json b/registries/codes.json index ba4d660..2769ba8 100644 --- a/registries/codes.json +++ b/registries/codes.json @@ -1302,11 +1302,6 @@ "endpoint": "datasets.options", "channel": "problem_details", "http_status": 422 - }, - { - "endpoint": "datasets.exports", - "channel": "problem_details", - "http_status": 422 } ], "lifecycle": "active" @@ -4154,11 +4149,6 @@ "endpoint": "datasets.options", "channel": "problem_details", "http_status": 422 - }, - { - "endpoint": "datasets.exports", - "channel": "problem_details", - "http_status": 422 } ], "lifecycle": "active" From ba5191bf4a6c7a5e035526ec8056679c971c2954 Mon Sep 17 00:00:00 2001 From: Jorge Rivera Date: Fri, 30 Oct 2026 01:53:23 +0100 Subject: [PATCH 5/7] docs(registry): tighten the MCP refusal and exports entries --- python/spec-kit/CHANGELOG.md | 22 ++++++++++++---------- schemas/registry/README.md | 2 +- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/python/spec-kit/CHANGELOG.md b/python/spec-kit/CHANGELOG.md index 4995721..041f919 100644 --- a/python/spec-kit/CHANGELOG.md +++ b/python/spec-kit/CHANGELOG.md @@ -264,19 +264,21 @@ and this package adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `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 an MCP tool result - carries, on endpoint `mcp.tools` and channel `mcp_refusal`: an unknown or - removed tool, a frame reference that lacks its content version, a frame - never stored, a frame aged out of the working tier, and a derived frame that - follows the latest data and has no stored computation tree to replay under a - shared period. `provenance_unavailable` (422) and + 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 never stored, a frame aged out of the working tier, and a derived frame + that follows the latest data and has no stored computation tree to replay + under a shared period. `provenance_unavailable` (422) and `licence_not_redistributable` (403) are on `datasets.exports`, the latter - also on `datasets.export_link`: an export of a derived frame with no stored - recipe, and an export whose source licence forbids redistribution. + also on `datasets.export_link`. They cover an export of a derived frame with + no stored recipe, and an export whose source licence forbids + redistribution. - Endpoint `mcp.tools` and channel `mcp_refusal` in `schemas/registry/code-registry.schema.json`, its README and - `tools/validate_registry.py`: the refusal envelope of an MCP tool result - (dfo-ai data-mcp, analysis-mcp, presentation-mcp). A mapping on it carries no - `http_status`, and `mcp_refusal` is a refusal-only channel. + `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`, and `mcp_refusal` is a refusal-only + channel. - 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 diff --git a/schemas/registry/README.md b/schemas/registry/README.md index f097c3a..7164303 100644 --- a/schemas/registry/README.md +++ b/schemas/registry/README.md @@ -88,7 +88,7 @@ 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 an MCP tool result carries: a `code`, a `retry_mode`, a message and a `fix`. It has no HTTP status. + - `mcp_refusal` is the refusal envelope an MCP tool result carries, made of a `code`, a `retry_mode`, a message and a `fix`. It has no HTTP status. `http_status` is present only when `channel` is `problem_details`. From 52e4e42b4ecf542920e70d53475881e4f5fa4666 Mon Sep 17 00:00:00 2001 From: Jorge Rivera Date: Fri, 30 Oct 2026 08:51:32 +0100 Subject: [PATCH 6/7] fix(registry): state what the MCP refusals mean and register the rest Definitions of frame_not_held, frame_expired, rolling_reanchor_unavailable and provenance_unavailable now state the conditions dfo-ai and dfo-apis serve them for, and provenance_unavailable takes the fix_request action. The mcp.tools endpoint and the mcp_refusal channel require each other in the schema. Eight codes only an MCP tool result carries join the registry, and rate_limited and transient_unavailable share their spelling with the proxy table, so the proxy check refuses only codes that reach a problem document or a no_data reason. datasets.export_link registers identity_quarantined, as datasets.files does. --- python/client-contract/CHANGELOG.md | 4 +- python/client-contract/README.md | 2 +- .../src/dfo_client_contract/actions.json | 2 +- python/spec-kit/CHANGELOG.md | 32 +++-- .../src/dfo_spec_kit/registry/codes.json | 133 +++++++++++++++++- registries/codes.json | 133 +++++++++++++++++- schemas/registry/README.md | 10 +- schemas/registry/code-registry.schema.json | 40 +++++- tools/tests/test_validate_client_contract.py | 8 +- tools/tests/test_validate_registry.py | 44 +++++- tools/validate_client_contract.py | 17 ++- 11 files changed, 388 insertions(+), 37 deletions(-) diff --git a/python/client-contract/CHANGELOG.md b/python/client-contract/CHANGELOG.md index cfd2adc..07f1d0c 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, and `licence_not_redistributable`, since a source's licence forbids the export for every caller. - 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 for the two refusals of the exports door: `provenance_unavailable` (`fix_request`), which rebuilding the frame clears, and `licence_not_redistributable` (`stop`). +- `tools/validate_client_contract.py` lets a proxy code share a registry code that reaches no problem document or no_data reason, as `rate_limited` and `transient_unavailable` do on `mcp.tools`. [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..16114bc 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. `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 96a3121..058ca4b 100644 --- a/python/client-contract/src/dfo_client_contract/actions.json +++ b/python/client-contract/src/dfo_client_contract/actions.json @@ -336,7 +336,7 @@ }, { "code": "provenance_unavailable", - "action": "stop" + "action": "fix_request" }, { "code": "provider_ambiguous", diff --git a/python/spec-kit/CHANGELOG.md b/python/spec-kit/CHANGELOG.md index 041f919..31f4df0 100644 --- a/python/spec-kit/CHANGELOG.md +++ b/python/spec-kit/CHANGELOG.md @@ -263,22 +263,31 @@ and this package adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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 an MCP tool result + 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 never stored, a frame aged out of the working tier, and a derived frame - that follows the latest data and has no stored computation tree to replay - under a shared 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 derived frame with - no stored recipe, and an export whose source licence forbids - redistribution. + 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 principal to publish a frame as, a server at its limit of calls, and + a service behind the server that failed or a lost frame not yet computable. - 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`, and `mcp_refusal` is a refusal-only - channel. + 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 @@ -304,7 +313,8 @@ and this package adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `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), - `service_unavailable` (503) and `internal_error` (500). + `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 2769ba8..b92c42f 100644 --- a/python/spec-kit/src/dfo_spec_kit/registry/codes.json +++ b/python/spec-kit/src/dfo_spec_kit/registry/codes.json @@ -908,6 +908,21 @@ ], "lifecycle": "active" }, + { + "code": "definition_not_held", + "classification": "refusal", + "definition": "No definition is registered under the id a request names. The definition store answered, so a new search finds an id it holds.", + "owning_contract": "mcp", + "owner": "platform", + "introduced_in": "v1", + "mappings": [ + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" + } + ], + "lifecycle": "active" + }, { "code": "definition_not_reproducible", "classification": "refusal", @@ -1091,6 +1106,21 @@ ], "lifecycle": "active" }, + { + "code": "derived_input_unrecoverable", + "classification": "refusal", + "definition": "An input frame was computed by an engine whose period labels 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" + }, { "code": "derived_input_unusable", "classification": "refusal", @@ -1346,6 +1376,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", @@ -1803,7 +1848,22 @@ { "code": "frame_expired", "classification": "refusal", - "definition": "The frame a reference names was stored and has since aged out of the working tier, so none of its artifacts remains servable.", + "definition": "The frame a reference names was stored and has since aged out: it passed an in-memory store's time limit, or the platform reports that every artifact of its version has expired.", + "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 on read, so the object does not match what was recorded when it was written. The same reference reaches the same object, so only a fresh result under a new reference clears it.", "owning_contract": "mcp", "owner": "platform", "introduced_in": "v1", @@ -1818,7 +1878,7 @@ { "code": "frame_not_held", "classification": "refusal", - "definition": "The frame a reference names was never stored here, or the stored identity and version do not belong together.", + "definition": "The frame a reference names is not held: it was never stored, a durable store's lifecycle rule has deleted it, a live store evicted it, or the platform holds no such version. The same code answers a reference whose identity and version do not belong together.", "owning_contract": "mcp", "owner": "platform", "introduced_in": "v1", @@ -2042,6 +2102,11 @@ "endpoint": "datasets.exports", "channel": "problem_details", "http_status": 500 + }, + { + "endpoint": "datasets.export_link", + "channel": "problem_details", + "http_status": 500 } ], "lifecycle": "active" @@ -3078,6 +3143,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", @@ -3526,7 +3606,7 @@ { "code": "provenance_unavailable", "classification": "refusal", - "definition": "A derived frame has no stored recipe, so its processing steps and input versions cannot be listed for an export.", + "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", @@ -3579,6 +3659,21 @@ ], "lifecycle": "active" }, + { + "code": "publish_not_configured", + "classification": "refusal", + "definition": "This deployment has no principal to publish a frame as, 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", @@ -3762,6 +3857,21 @@ ], "lifecycle": "active" }, + { + "code": "rate_limited", + "classification": "refusal", + "definition": "The server is at its limit of calls, running at once or accepted over time, 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" + }, { "code": "recipe_not_declared", "classification": "refusal", @@ -4495,7 +4605,7 @@ { "code": "rolling_reanchor_unavailable", "classification": "refusal", - "definition": "A derived frame whose period follows the latest data cannot be computed again under the latest period shared with the other frames in the request, because its stored recipe has no computation tree to replay.", + "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", @@ -5921,6 +6031,21 @@ ], "lifecycle": "active" }, + { + "code": "transient_unavailable", + "classification": "refusal", + "definition": "A service behind the server 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" + }, { "code": "un_system_unavailable", "classification": "warning", diff --git a/registries/codes.json b/registries/codes.json index 2769ba8..b92c42f 100644 --- a/registries/codes.json +++ b/registries/codes.json @@ -908,6 +908,21 @@ ], "lifecycle": "active" }, + { + "code": "definition_not_held", + "classification": "refusal", + "definition": "No definition is registered under the id a request names. The definition store answered, so a new search finds an id it holds.", + "owning_contract": "mcp", + "owner": "platform", + "introduced_in": "v1", + "mappings": [ + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" + } + ], + "lifecycle": "active" + }, { "code": "definition_not_reproducible", "classification": "refusal", @@ -1091,6 +1106,21 @@ ], "lifecycle": "active" }, + { + "code": "derived_input_unrecoverable", + "classification": "refusal", + "definition": "An input frame was computed by an engine whose period labels 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" + }, { "code": "derived_input_unusable", "classification": "refusal", @@ -1346,6 +1376,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", @@ -1803,7 +1848,22 @@ { "code": "frame_expired", "classification": "refusal", - "definition": "The frame a reference names was stored and has since aged out of the working tier, so none of its artifacts remains servable.", + "definition": "The frame a reference names was stored and has since aged out: it passed an in-memory store's time limit, or the platform reports that every artifact of its version has expired.", + "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 on read, so the object does not match what was recorded when it was written. The same reference reaches the same object, so only a fresh result under a new reference clears it.", "owning_contract": "mcp", "owner": "platform", "introduced_in": "v1", @@ -1818,7 +1878,7 @@ { "code": "frame_not_held", "classification": "refusal", - "definition": "The frame a reference names was never stored here, or the stored identity and version do not belong together.", + "definition": "The frame a reference names is not held: it was never stored, a durable store's lifecycle rule has deleted it, a live store evicted it, or the platform holds no such version. The same code answers a reference whose identity and version do not belong together.", "owning_contract": "mcp", "owner": "platform", "introduced_in": "v1", @@ -2042,6 +2102,11 @@ "endpoint": "datasets.exports", "channel": "problem_details", "http_status": 500 + }, + { + "endpoint": "datasets.export_link", + "channel": "problem_details", + "http_status": 500 } ], "lifecycle": "active" @@ -3078,6 +3143,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", @@ -3526,7 +3606,7 @@ { "code": "provenance_unavailable", "classification": "refusal", - "definition": "A derived frame has no stored recipe, so its processing steps and input versions cannot be listed for an export.", + "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", @@ -3579,6 +3659,21 @@ ], "lifecycle": "active" }, + { + "code": "publish_not_configured", + "classification": "refusal", + "definition": "This deployment has no principal to publish a frame as, 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", @@ -3762,6 +3857,21 @@ ], "lifecycle": "active" }, + { + "code": "rate_limited", + "classification": "refusal", + "definition": "The server is at its limit of calls, running at once or accepted over time, 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" + }, { "code": "recipe_not_declared", "classification": "refusal", @@ -4495,7 +4605,7 @@ { "code": "rolling_reanchor_unavailable", "classification": "refusal", - "definition": "A derived frame whose period follows the latest data cannot be computed again under the latest period shared with the other frames in the request, because its stored recipe has no computation tree to replay.", + "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", @@ -5921,6 +6031,21 @@ ], "lifecycle": "active" }, + { + "code": "transient_unavailable", + "classification": "refusal", + "definition": "A service behind the server 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" + }, { "code": "un_system_unavailable", "classification": "warning", diff --git a/schemas/registry/README.md b/schemas/registry/README.md index 7164303..5d92606 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. @@ -39,7 +39,7 @@ Each entry: - **`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 one exception: an MCP tool result may also carry a code an HTTP route serves, and that code has no `mcp.tools` mapping (see `mcp.tools` below). `(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. @@ -67,7 +67,7 @@ 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`, a refusal an MCP tool result carries (dfo-ai data-mcp, analysis-mcp, presentation-mcp). It uses the `mcp_refusal` channel alone, so its mappings carry no `http_status`, and a code on it is not returned by any HTTP route. + The MCP endpoint is `mcp.tools`, which holds the codes only an MCP tool result carries. dfo-ai data-mcp, analysis-mcp and presentation-mcp write them, and so does the harness combined server, which writes `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` and no HTTP route returns a code that maps here alone. A code an HTTP route also serves keeps its HTTP mappings and appears in MCP refusals with no `mcp.tools` row, as `request_invalid` and `internal_error` do. 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. @@ -75,7 +75,7 @@ Retiring a door is a schema change, reviewed as one: dropping a value from `code `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. @@ -94,7 +94,7 @@ Retiring a door is a schema change, reviewed as one: dropping a value from `code ## 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 c05ef90..1dbbc75 100644 --- a/schemas/registry/code-registry.schema.json +++ b/schemas/registry/code-registry.schema.json @@ -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 code mapped to an HTTP route; see schemas/registry/README.md.", "type": "array", "minItems": 1, "items": { @@ -195,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": { diff --git a/tools/tests/test_validate_client_contract.py b/tools/tests/test_validate_client_contract.py index 700a78e..39ad15c 100644 --- a/tools/tests/test_validate_client_contract.py +++ b/tools/tests/test_validate_client_contract.py @@ -1424,6 +1424,11 @@ 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", + ), ( "warning-codes.json", _drop_action("ref_digest_changed"), @@ -2206,7 +2211,8 @@ 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", "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..188d6d3 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 that reaches no problem document + or no_data reason, since neither has an `actions.json` row. 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` @@ -4252,13 +4254,22 @@ def validate() -> list[str]: "client-codes.json": {row["code"] for row in client_rows}, } proxy_codes = _load(PACKAGE_DIR / "proxy-codes.json") + # A client raises a proxy code by its status, so a registry code clashes only + # where it reaches a client with an action of its own. + with_actions = { + entry["code"] + for entry in registry["entries"] + if _ACTION_CHANNELS & {mapping["channel"] 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": with_actions}, ) ) proxy_rows = [] if errors else proxy_codes["codes"] From 2dd2400a36d592bd3ca814aa2eff30bd016433e3 Mon Sep 17 00:00:00 2001 From: Jorge Rivera Date: Fri, 30 Oct 2026 10:04:06 +0100 Subject: [PATCH 7/7] fix(registry): map every code the MCP servers write and state the surface mcp.tools now holds every code data-mcp, analysis-mcp, presentation-mcp and the combined server write, so a mapping states where a code appears. presentation-mcp also forwards platform codes it does not write itself. A proxy code may share a spelling only with a code whose every mapping is mcp_refusal, and rate_limited and transient_unavailable are worded to hold on both surfaces. provenance_unavailable takes the stop action, since a recipe root that cannot be cited survives a rebuild. Definitions name no store mechanics, and the README states every field of the mcp_refusal envelope. --- python/client-contract/CHANGELOG.md | 6 +- python/client-contract/README.md | 2 +- .../src/dfo_client_contract/actions.json | 2 +- python/spec-kit/CHANGELOG.md | 11 +- .../src/dfo_spec_kit/registry/codes.json | 232 +++++++++++++++++- registries/codes.json | 232 +++++++++++++++++- schemas/registry/README.md | 8 +- schemas/registry/code-registry.schema.json | 4 +- tools/tests/test_validate_client_contract.py | 8 + tools/validate_client_contract.py | 23 +- 10 files changed, 490 insertions(+), 38 deletions(-) diff --git a/python/client-contract/CHANGELOG.md b/python/client-contract/CHANGELOG.md index 07f1d0c..aa12883 100644 --- a/python/client-contract/CHANGELOG.md +++ b/python/client-contract/CHANGELOG.md @@ -35,11 +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, and `licence_not_redistributable`, since a source's licence forbids the export for every caller. +- 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 for the two refusals of the exports door: `provenance_unavailable` (`fix_request`), which rebuilding the frame clears, and `licence_not_redistributable` (`stop`). -- `tools/validate_client_contract.py` lets a proxy code share a registry code that reaches no problem document or no_data reason, as `rate_limited` and `transient_unavailable` do on `mcp.tools`. +- `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 16114bc..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. 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. `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 058ca4b..96a3121 100644 --- a/python/client-contract/src/dfo_client_contract/actions.json +++ b/python/client-contract/src/dfo_client_contract/actions.json @@ -336,7 +336,7 @@ }, { "code": "provenance_unavailable", - "action": "fix_request" + "action": "stop" }, { "code": "provider_ambiguous", diff --git a/python/spec-kit/CHANGELOG.md b/python/spec-kit/CHANGELOG.md index 31f4df0..6c5e2eb 100644 --- a/python/spec-kit/CHANGELOG.md +++ b/python/spec-kit/CHANGELOG.md @@ -280,8 +280,15 @@ and this package adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 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 principal to publish a frame as, a server at its limit of calls, and - a service behind the server that failed or a lost frame not yet computable. + 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 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 b92c42f..b35f039 100644 --- a/python/spec-kit/src/dfo_spec_kit/registry/codes.json +++ b/python/spec-kit/src/dfo_spec_kit/registry/codes.json @@ -28,6 +28,10 @@ "endpoint": "datasets.exports", "channel": "problem_details", "http_status": 429 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -344,6 +348,10 @@ "endpoint": "datasets.exports", "channel": "problem_details", "http_status": 501 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -574,6 +582,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -590,6 +602,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -754,6 +770,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -770,6 +790,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -786,6 +810,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -802,6 +830,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -889,6 +921,10 @@ "endpoint": "datasets.exports", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -911,7 +947,7 @@ { "code": "definition_not_held", "classification": "refusal", - "definition": "No definition is registered under the id a request names. The definition store answered, so a new search finds an id it holds.", + "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", @@ -1102,6 +1138,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -1109,7 +1149,7 @@ { "code": "derived_input_unrecoverable", "classification": "refusal", - "definition": "An input frame was computed by an engine whose period labels 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.", + "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", @@ -1133,6 +1173,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -1352,6 +1396,10 @@ "dataset_resubmit" ], "channel": "no_data_reason" + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -1703,6 +1751,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -1773,6 +1825,10 @@ "endpoint": "datasets.exports", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -1825,6 +1881,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -1841,6 +1901,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -1848,7 +1912,7 @@ { "code": "frame_expired", "classification": "refusal", - "definition": "The frame a reference names was stored and has since aged out: it passed an in-memory store's time limit, or the platform reports that every artifact of its version has expired.", + "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", @@ -1863,7 +1927,7 @@ { "code": "frame_integrity_failed", "classification": "refusal", - "definition": "A stored frame failed an integrity check on read, so the object does not match what was recorded when it was written. The same reference reaches the same object, so only a fresh result under a new reference clears it.", + "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", @@ -1878,7 +1942,7 @@ { "code": "frame_not_held", "classification": "refusal", - "definition": "The frame a reference names is not held: it was never stored, a durable store's lifecycle rule has deleted it, a live store evicted it, or the platform holds no such version. The same code answers a reference whose identity and version do not belong together.", + "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", @@ -1933,6 +1997,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -1959,6 +2027,10 @@ "endpoint": "presentation.shares", "channel": "problem_details", "http_status": 404 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -1975,6 +2047,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -2208,6 +2284,10 @@ "endpoint": "datasets.export_link", "channel": "problem_details", "http_status": 500 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -2271,6 +2351,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -2287,6 +2371,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -2308,6 +2396,10 @@ "endpoint": "datasets.export_link", "channel": "problem_details", "http_status": 403 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -2354,6 +2446,10 @@ "endpoint": "datasets.exports", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -2503,6 +2599,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -2667,6 +2767,10 @@ "endpoint": "graph.documentation", "channel": "problem_details", "http_status": 404 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -2683,6 +2787,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -3088,6 +3196,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -3107,6 +3219,10 @@ ], "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -3170,6 +3286,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -3309,6 +3429,10 @@ "endpoint": "datasets.exports", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -3514,6 +3638,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -3615,6 +3743,10 @@ "endpoint": "datasets.exports", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -3662,7 +3794,7 @@ { "code": "publish_not_configured", "classification": "refusal", - "definition": "This deployment has no principal to publish a frame as, so it cannot publish a frame to the platform.", + "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", @@ -3764,6 +3896,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -3780,6 +3916,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -3796,6 +3936,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -3860,7 +4004,7 @@ { "code": "rate_limited", "classification": "refusal", - "definition": "The server is at its limit of calls, running at once or accepted over time, so it refused this one. The identical call is accepted after the wait the refusal names.", + "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", @@ -4057,6 +4201,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -4206,6 +4354,10 @@ "endpoint": "datasets.exports", "channel": "problem_details", "http_status": 400 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -4347,6 +4499,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -4566,6 +4722,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -4582,6 +4742,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -4649,6 +4813,10 @@ "endpoint": "datasets.exports", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -4872,6 +5040,10 @@ "endpoint": "datasets.export_link", "channel": "problem_details", "http_status": 503 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -4971,6 +5143,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -4987,6 +5163,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -5003,6 +5183,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -5019,6 +5203,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -5176,6 +5364,10 @@ "endpoint": "skills.resource", "channel": "problem_details", "http_status": 404 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -5249,6 +5441,10 @@ "endpoint": "skills.resource", "channel": "problem_details", "http_status": 404 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -5707,6 +5903,10 @@ "endpoint": "datasets.exports", "channel": "problem_details", "http_status": 409 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -5833,6 +6033,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -6034,7 +6238,7 @@ { "code": "transient_unavailable", "classification": "refusal", - "definition": "A service behind the server 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.", + "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", @@ -6104,6 +6308,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -6323,6 +6531,10 @@ "endpoint": "datasets.exports", "channel": "problem_details", "http_status": 502 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -6374,6 +6586,10 @@ "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 b92c42f..b35f039 100644 --- a/registries/codes.json +++ b/registries/codes.json @@ -28,6 +28,10 @@ "endpoint": "datasets.exports", "channel": "problem_details", "http_status": 429 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -344,6 +348,10 @@ "endpoint": "datasets.exports", "channel": "problem_details", "http_status": 501 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -574,6 +582,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -590,6 +602,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -754,6 +770,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -770,6 +790,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -786,6 +810,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -802,6 +830,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -889,6 +921,10 @@ "endpoint": "datasets.exports", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -911,7 +947,7 @@ { "code": "definition_not_held", "classification": "refusal", - "definition": "No definition is registered under the id a request names. The definition store answered, so a new search finds an id it holds.", + "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", @@ -1102,6 +1138,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -1109,7 +1149,7 @@ { "code": "derived_input_unrecoverable", "classification": "refusal", - "definition": "An input frame was computed by an engine whose period labels 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.", + "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", @@ -1133,6 +1173,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -1352,6 +1396,10 @@ "dataset_resubmit" ], "channel": "no_data_reason" + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -1703,6 +1751,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -1773,6 +1825,10 @@ "endpoint": "datasets.exports", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -1825,6 +1881,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -1841,6 +1901,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -1848,7 +1912,7 @@ { "code": "frame_expired", "classification": "refusal", - "definition": "The frame a reference names was stored and has since aged out: it passed an in-memory store's time limit, or the platform reports that every artifact of its version has expired.", + "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", @@ -1863,7 +1927,7 @@ { "code": "frame_integrity_failed", "classification": "refusal", - "definition": "A stored frame failed an integrity check on read, so the object does not match what was recorded when it was written. The same reference reaches the same object, so only a fresh result under a new reference clears it.", + "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", @@ -1878,7 +1942,7 @@ { "code": "frame_not_held", "classification": "refusal", - "definition": "The frame a reference names is not held: it was never stored, a durable store's lifecycle rule has deleted it, a live store evicted it, or the platform holds no such version. The same code answers a reference whose identity and version do not belong together.", + "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", @@ -1933,6 +1997,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -1959,6 +2027,10 @@ "endpoint": "presentation.shares", "channel": "problem_details", "http_status": 404 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -1975,6 +2047,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -2208,6 +2284,10 @@ "endpoint": "datasets.export_link", "channel": "problem_details", "http_status": 500 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -2271,6 +2351,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -2287,6 +2371,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -2308,6 +2396,10 @@ "endpoint": "datasets.export_link", "channel": "problem_details", "http_status": 403 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -2354,6 +2446,10 @@ "endpoint": "datasets.exports", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -2503,6 +2599,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -2667,6 +2767,10 @@ "endpoint": "graph.documentation", "channel": "problem_details", "http_status": 404 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -2683,6 +2787,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -3088,6 +3196,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -3107,6 +3219,10 @@ ], "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -3170,6 +3286,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -3309,6 +3429,10 @@ "endpoint": "datasets.exports", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -3514,6 +3638,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -3615,6 +3743,10 @@ "endpoint": "datasets.exports", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -3662,7 +3794,7 @@ { "code": "publish_not_configured", "classification": "refusal", - "definition": "This deployment has no principal to publish a frame as, so it cannot publish a frame to the platform.", + "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", @@ -3764,6 +3896,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -3780,6 +3916,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -3796,6 +3936,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -3860,7 +4004,7 @@ { "code": "rate_limited", "classification": "refusal", - "definition": "The server is at its limit of calls, running at once or accepted over time, so it refused this one. The identical call is accepted after the wait the refusal names.", + "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", @@ -4057,6 +4201,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -4206,6 +4354,10 @@ "endpoint": "datasets.exports", "channel": "problem_details", "http_status": 400 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -4347,6 +4499,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -4566,6 +4722,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -4582,6 +4742,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -4649,6 +4813,10 @@ "endpoint": "datasets.exports", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -4872,6 +5040,10 @@ "endpoint": "datasets.export_link", "channel": "problem_details", "http_status": 503 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -4971,6 +5143,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -4987,6 +5163,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -5003,6 +5183,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -5019,6 +5203,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -5176,6 +5364,10 @@ "endpoint": "skills.resource", "channel": "problem_details", "http_status": 404 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -5249,6 +5441,10 @@ "endpoint": "skills.resource", "channel": "problem_details", "http_status": 404 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -5707,6 +5903,10 @@ "endpoint": "datasets.exports", "channel": "problem_details", "http_status": 409 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -5833,6 +6033,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -6034,7 +6238,7 @@ { "code": "transient_unavailable", "classification": "refusal", - "definition": "A service behind the server 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.", + "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", @@ -6104,6 +6308,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -6323,6 +6531,10 @@ "endpoint": "datasets.exports", "channel": "problem_details", "http_status": 502 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -6374,6 +6586,10 @@ "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 5d92606..ec64821 100644 --- a/schemas/registry/README.md +++ b/schemas/registry/README.md @@ -39,7 +39,7 @@ Each entry: - **`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, with one exception: an MCP tool result may also carry a code an HTTP route serves, and that code has no `mcp.tools` mapping (see `mcp.tools` below). `(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. @@ -67,7 +67,9 @@ 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 the codes only an MCP tool result carries. dfo-ai data-mcp, analysis-mcp and presentation-mcp write them, and so does the harness combined server, which writes `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` and no HTTP route returns a code that maps here alone. A code an HTTP route also serves keeps its HTTP mappings and appears in MCP refusals with no `mcp.tools` row, as `request_invalid` and `internal_error` do. + 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. @@ -88,7 +90,7 @@ 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 an MCP tool result carries, made of a `code`, a `retry_mode`, a message and a `fix`. It has no HTTP status. + - `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`. diff --git a/schemas/registry/code-registry.schema.json b/schemas/registry/code-registry.schema.json index 1dbbc75..22217f2 100644 --- a/schemas/registry/code-registry.schema.json +++ b/schemas/registry/code-registry.schema.json @@ -70,7 +70,7 @@ "minLength": 1 }, "mappings": { - "description": "Every endpoint/door/channel combination this code is allowed to appear on. An MCP tool result may also carry a code mapped to an HTTP route; see schemas/registry/README.md.", + "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": { @@ -149,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. mcp_refusal is the refusal envelope an MCP tool result carries, a `code` with a `retry_mode`, a message and a `fix`, which has no HTTP status.", + "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", diff --git a/tools/tests/test_validate_client_contract.py b/tools/tests/test_validate_client_contract.py index 39ad15c..f17cd0e 100644 --- a/tools/tests/test_validate_client_contract.py +++ b/tools/tests/test_validate_client_contract.py @@ -1429,6 +1429,13 @@ def change(doc: Any) -> None: _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"), @@ -2213,6 +2220,7 @@ def change(doc: Any) -> None: "accepted-operation-reads-done", "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/validate_client_contract.py b/tools/validate_client_contract.py index 188d6d3..2f03997 100644 --- a/tools/validate_client_contract.py +++ b/tools/validate_client_contract.py @@ -35,9 +35,9 @@ 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, except - that a proxy code may share a registry code that reaches no problem document - or no_data reason, since neither has an `actions.json` row. The warning - codes are those the client rules issue: `ref_digest_changed`, + 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` @@ -500,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( @@ -4254,12 +4254,12 @@ def validate() -> list[str]: "client-codes.json": {row["code"] for row in client_rows}, } proxy_codes = _load(PACKAGE_DIR / "proxy-codes.json") - # A client raises a proxy code by its status, so a registry code clashes only - # where it reaches a client with an action of its own. - with_actions = { + # 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 _ACTION_CHANNELS & {mapping["channel"] for mapping in entry["mappings"]} + if all(mapping["channel"] == "mcp_refusal" for mapping in entry["mappings"]) } errors = _schema_problems( "proxy-codes.json", validators["proxy-codes.schema.json"], proxy_codes @@ -4269,7 +4269,10 @@ def validate() -> list[str]: or _check_other_codes( "proxy-codes.json", [row["code"] for row in proxy_codes["codes"]], - {**taken, "registries/codes.json": with_actions}, + { + **taken, + "registries/codes.json": taken["registries/codes.json"] - mcp_only, + }, ) ) proxy_rows = [] if errors else proxy_codes["codes"]