Skip to content
4 changes: 3 additions & 1 deletion python/client-contract/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,11 @@ All notable changes to the `dfo-spec-kit-client-contract` Python package will be
- A derived member's root records its rendering in `root_rendering`, the artifact selector's members and `groupby`, the sum DFO read the root with as `options.groupby` states it: `by`, one or more distinct column names, `op` `sum`, and `levels`, a depth from 0 to 99 for each kept hierarchical axis, absent when none is set. A breakdown or a sum over StatVars reads a root that way. The artifact's `rendering` admits no `groupby`. `ref-derived-grouped-root` is accepted, its `by` sorted as DFO records it and its identities computed by the dfo-apis analysis runner over the scenario's rows summed to their purpose's sector code, and `ref-derived-root-groupby-op-rejected` and `ref-artifact-groupby-rejected` are rejected. The validator checks a summed root's rendering against its scenario's build without its `groupby`, which no recorded build posts.
- The analysis and publication rules DFO applies, each with vectors and a reference implementation in `tests/test_analysis_rules.py`: `output-digest`, the `out2` digest of a derived frame's values with the `stat_var`, members, period and unit each belongs to, without `measurementMethod`, `provenance`, `status` or a unit's `recipe`, reading the variable `compare`, `sum_of`, `share_of` or `quotient_of` writes under an `mv_` name hashed from its verb and its inputs' variables, 35 characters at any depth, so an engine release or upstream rebuild that moves no number keeps the digest, with 22 canonicalisation vectors, each minted name given with its name document and one naming a `quotient_of` over a `sum_of` by the inner step's name; `output-comparison`, how `==` and `same_data` compare two refs, with 12 vectors; `operation-outputs`, `share_of` in per cent under `Percent`, `join` in long format by `stat_var`, `compare` as data minus other, `sum_of` as data plus other, and `growth` as a fraction in `value` with no unit, leaving out a member with no value at an end of its window, a relative change over a zero or sign-crossing base and a change that is not finite, and refusing two rows of one member in one period with `role_ambiguous`, with 10 vectors; `rolling-composition`, one latest period per recipe tree, refusing rolling steps over different populations with `rolling_populations_differ` even when they settle on one year, with 6 vectors; and `publication`, a share's numbered slots that keep their URLs across a live replace, a snapshot that refuses `share_not_replaceable`, and the write token checks, with 8 vectors.
- `actions.json` rows for `allowance_exhausted` (`wait_for_reset`), `rolling_populations_differ`, `several_members`, `share_not_replaceable` and `write_token_required` (`fix_request`), and `write_token_invalid` (`stop`).
- The `stop` action, class `PermissionDeniedError`, for a refusal no permission or credential the caller can obtain clears: `write_token_invalid`, since a share takes only the write token issued when it was created and a share created without one takes none.
- The `stop` action, class `PermissionDeniedError`, for a refusal no permission or credential the caller can obtain clears: `write_token_invalid`, since a share takes only the write token issued when it was created and a share created without one takes none, `licence_not_redistributable`, since a source's licence forbids the export for every caller, and `provenance_unavailable`, since a recipe root that cannot be cited survives a rebuild.
- The retry rules: a refusal whose code's action is `wait_for_reset` is never retried and raises `QuotaExceededError` at once, whatever its document and status say, and a refusal whose `Retry-After` is longer than 60 seconds (`RETRY_AFTER_MAX_S`) raises at once with that wait, a refused poll included. `search-retry-after-past-cap` pins the cap.
- `series_left_out` in `warning-codes.json`, class `SeriesLeftOutWarning`, and the `series-left-out` rule with 8 vectors: a fetch of derived members issues it once when any member's derived manifest lists left-out series in `left_out` or says `left_out_complete` false, a null `left_out` issues nothing, and a strict fetch raises it in place of its data. `tools/validate_client_contract.py` takes the code from the rule.
- `actions.json` rows (`fix_request`) for the 34 refusal codes the analysis engine serves on `datasets.analysis`, including `compare_no_key_matched` and `sum_of_no_key_matched`.
- `actions.json` rows (`stop`) for the two refusals of the exports door, `provenance_unavailable` and `licence_not_redistributable`.
- `tools/validate_client_contract.py` lets a proxy code share a registry code whose every mapping is `mcp_refusal`, as `rate_limited` and `transient_unavailable` do.

[unreleased]: https://github.com/DevelopmentFinanceObservatory/spec-kit/commits/main/python
2 changes: 1 addition & 1 deletion python/client-contract/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ The required members are each part's `status` and its payload (`spec`, `candidat
| `report` | `ServerError` | Reports the trace id, after the client's retries |
| `upgrade` | `ContractError` | Upgrades the client |

A code that maps to `no_data_reason` also carries `gap_kind`, the kind of gap a no_data part with that reason becomes: `choice`, `no_data`, `not_understood`, `unsupported` or `timed_out`. `timed_out` is a part Search left unresolved at its time limit. Sending the question again, or a narrower one, may answer it. `choose` goes with `choice` and only with it. `choose` and `ask_data_question` name a next step inside an answer, so a code that also reaches a problem document takes another action. The website proxy answers its own refusals with problem documents of the same shape, with `type`, `title`, `status`, `detail`, `code`, `retryable` and `trace_id`, the trace id repeated in the `x-dfo-trace-id` header. `proxy-codes.json` lists their codes with their status, `rate_limited` at 429, which also sends `Retry-After`, and `transient_unavailable` at 502, both retryable. The validator refuses a problem document whose code is neither an active registry code nor a proxy code at that status. No proxy code has a row here, so a client raises a proxy refusal with the class its status maps to: `RateLimitError` for `rate_limited`, which `search-proxy-rate-limited` pins, and `ServerError` for `transient_unavailable`, which `search-proxy-unavailable` pins. `capability_unavailable` is a well-formed request for a format, reshape or conversion class the deployment does not enable, answered 501. Its action is `fix_request`, since the caller asks for another one, and `fetch-spec-capability-unavailable` pins it. `write_token_invalid` takes `stop`: a share takes only the write token issued when it was created, and a share created without one takes none, so no scope or new token lets the caller replace or delete it. `allowance_exhausted` takes `wait_for_reset`, and a client raises it at once as `QuotaExceededError` with its `Retry-After`, the time until the allowance resets, as the retry rules below state.
A code that maps to `no_data_reason` also carries `gap_kind`, the kind of gap a no_data part with that reason becomes: `choice`, `no_data`, `not_understood`, `unsupported` or `timed_out`. `timed_out` is a part Search left unresolved at its time limit. Sending the question again, or a narrower one, may answer it. `choose` goes with `choice` and only with it. `choose` and `ask_data_question` name a next step inside an answer, so a code that also reaches a problem document takes another action. The website proxy answers its own refusals with problem documents of the same shape, with `type`, `title`, `status`, `detail`, `code`, `retryable` and `trace_id`, the trace id repeated in the `x-dfo-trace-id` header. `proxy-codes.json` lists their codes with their status, `rate_limited` at 429, which also sends `Retry-After`, and `transient_unavailable` at 502, both retryable. Both are also registered on `mcp.tools`, which reaches no problem document, so the proxy's use of them takes no row. The validator refuses a problem document whose code is neither an active registry code nor a proxy code at that status. No proxy code has a row here, so a client raises a proxy refusal with the class its status maps to: `RateLimitError` for `rate_limited`, which `search-proxy-rate-limited` pins, and `ServerError` for `transient_unavailable`, which `search-proxy-unavailable` pins. `capability_unavailable` is a well-formed request for a format, reshape or conversion class the deployment does not enable, answered 501. Its action is `fix_request`, since the caller asks for another one, and `fetch-spec-capability-unavailable` pins it. `write_token_invalid` takes `stop`: a share takes only the write token issued when it was created, and a share created without one takes none, so no scope or new token lets the caller replace or delete it. `licence_not_redistributable` takes `stop` too: a source's licence forbids the export for every caller, so no scope or credential serves the file. So does `provenance_unavailable`: a recipe root that cannot be cited survives a rebuild, so nothing the caller can change names the frame's sources. `allowance_exhausted` takes `wait_for_reset`, and a client raises it at once as `QuotaExceededError` with its `Retry-After`, the time until the allowance resets, as the retry rules below state.

A client maps a refusal whose code has no row, or that carries no code, on its HTTP status:

Expand Down
8 changes: 8 additions & 0 deletions python/client-contract/src/dfo_client_contract/actions.json
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,10 @@
"code": "join_output_collision",
"action": "fix_request"
},
{
"code": "licence_not_redistributable",
"action": "stop"
},
{
"code": "member_not_resolvable",
"action": "fix_request"
Expand Down Expand Up @@ -330,6 +334,10 @@
"code": "population_empty",
"action": "fix_request"
},
{
"code": "provenance_unavailable",
"action": "stop"
},
{
"code": "provider_ambiguous",
"action": "fix_request"
Expand Down
62 changes: 62 additions & 0 deletions python/spec-kit/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,68 @@ and this package adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
mirror: a 422 refusal on `search.resolve` for a `dataset_resubmit` whose
identity names an open role, when the StatVars the definition elects declare
no role on that axis, so the definition cannot give one series per member.
- Codes `tool_not_found`, `frame_ref_incomplete`, `frame_not_held`,
`frame_expired`, `rolling_reanchor_unavailable`, `provenance_unavailable`
and `licence_not_redistributable` in `registries/codes.json` and its bundled
mirror, all refusals. The first five are refusals only an MCP tool result
carries, on endpoint `mcp.tools` and channel `mcp_refusal`. They cover an
unknown or removed tool, a frame reference that lacks its content version, a
frame not held, a frame aged out, and a rolling computation with a derived
input that has no stored recipe to compute again under the shared latest
period. `provenance_unavailable` (422) and `licence_not_redistributable`
(403) are on `datasets.exports`, the latter also on `datasets.export_link`.
They cover an export of a frame whose sources and licences cannot be named,
and an export whose source licence forbids redistribution.
- Codes `definition_not_held`, `derived_input_unrecoverable`,
`entity_not_found`, `frame_integrity_failed`, `operation_still_processing`,
`publish_not_configured`, `rate_limited` and `transient_unavailable` in
`registries/codes.json` and its bundled
mirror, all refusals on `mcp.tools` and `mcp_refusal`. They cover an id with
no registered definition, an input frame whose period labels do not state
their years, a scope name that gives no entity, a stored frame that fails an
integrity check, a build unfinished when the call's wait ended, a deployment
with no identity to publish a frame under, a service at a limit on calls,
and a service behind the one called that failed or a lost frame not yet
computable. The website proxy's `proxy-codes.json` holds the proxy surface
of `rate_limited` and `transient_unavailable`.
- `mcp.tools` mappings on the 54 existing codes the MCP servers also write, so
the endpoint holds every code they write: each value of the data-mcp and
analysis-mcp `ToolErrorCode` enums and each code presentation-mcp and the
harness combined server write. Platform codes presentation-mcp only forwards
stay on their `presentation.*` mappings.
- Endpoint `mcp.tools` and channel `mcp_refusal` in
`schemas/registry/code-registry.schema.json`, its README and
`tools/validate_registry.py`. They describe the refusal envelope of an MCP
tool result (dfo-ai data-mcp, analysis-mcp, presentation-mcp). A mapping on
the endpoint carries no `http_status`, `mcp_refusal` is a refusal-only
channel, and the schema requires each of the two for the other.
- Endpoints `datasets.exports` and `datasets.export_link` in
`schemas/registry/code-registry.schema.json` and its README.
`datasets.exports` is `POST /api/datasets/exports`, which prepares an export
file of a stored frame. It carries `request_invalid` (400), `spec_not_found`
(404), `snapshot_not_found` (404), `snapshot_revoked` (410),
`artifact_expired` (410), `identity_quarantined` (500), `over_ceiling`
(422), `provenance_unavailable` (422), `licence_not_redistributable` (403),
`allowance_exhausted` (429), `service_unavailable` (503), `internal_error`
(500), `capability_unavailable` (501), `excel_limit` (422),
`excel_illegal_character` (422), `parquet_size_bound` (422) and
`rollup_not_summable` (422). A prepare that names only a `spec_id`
resolves its newest version, which can rebuild it, so the endpoint also
carries the codes a rebuild raises on `datasets.definition`, at the same
statuses: `spec_not_exportable` (409), `identity_collision` and
`members_not_held` (500), `upstream_error` (502),
`upstream_version_changed` (503), `timeout` (504), and
`collection_probe_ceiling`, `column_unknown`, `default_facet_undefined`,
`definition_not_reproducible`, `excluded_by_registry`,
`facet_binding_unsupported`, `facet_not_available`, `invalid_role_ref`,
`member_not_resolvable`, `rate_coverage_gap`, `recipe_not_declared`,
`role_incompatible` and
`role_signature_incompatible` (422). `datasets.export_link` is
`GET /api/datasets/exports/{export_id}`, which answers a 302 redirect to the
file's download link. It carries `not_found` (404),
`snapshot_revoked` (410), `licence_not_redistributable` (403),
`identity_quarantined` (500), `service_unavailable` (503) and
`internal_error` (500).
- Codes `requested_option_unavailable` and `conflicting_requested_options` in
`registries/codes.json` and its bundled mirror: 422 refusals on
`search.resolve` for a `dataset_resubmit` that posts `scope_entries`.
Expand Down
Loading
Loading