Repository navigation
Conversation
…f323ba6c7f Synced-from: ONEcampaign/dfo-spec-kit@5d381d8322288dc77411baa70c0dcff323ba6c7f
The $id values in the schemas stay as they are. The sync-guard workflow is removed.
ci: run spec-kit CI on dev and main
Schema $id values now resolve under raw.githubusercontent.com/DevelopmentFinanceObservatory/spec-kit/main, the manifest digest for intent.v0.json is recomputed, and the schema and tools READMEs and EXPORTS.md name the public repositories.
chore: point schema ids at the public repository
registries/codes.json and its bundled mirror gain whole_total_available, a search.resolve warning on the raw_text, parsed and dataset_resubmit doors. It flags an answer that lists each member of a flow side whose dataset publishes the total of that side's whole population for the answer's years, so a sum of the listed members can differ from the published figure.
…lasses The warning's `whole_of` param lists the member classes the total covers: official, private or residual.
…otal-available feat(codes): register the whole_total_available search warning
…10) * feat(registry): map the datasets file selector and versions refusals `GET /api/datasets/{id}/{content_version}/file` serves under `datasets.files` and refuses `request_invalid` (400), `excel_limit`, `excel_illegal_character`, `parquet_size_bound`, `rollup_not_summable` and `capability_unavailable` (422), and `internal_error` (500). The open `GET /api/datasets/{id}/versions` serves under `datasets.snapshot` and refuses `request_invalid` (400). The registry README names both routes. * fix(registry): map capability_unavailable on datasets.files to 501 The platform resolves one status per code, and every other endpoint answers capability_unavailable with 501.
dfo-spec-kit-client-contract (import dfo_client_contract) holds what the DFO Python and TypeScript clients generate from and what dfo-apis checks its app against: the served OpenAPI document, the strict fields clients act on, the next step for every active refusal code, the scenario and DatasetRef record formats, and rule vectors for the credentials file, key format and cache directory. tools/validate_client_contract.py checks the package data against the code registry and the served document.
tools/capture_client_contract.py captures the dfo-apis OpenAPI document in process and records scenarios against a base URL, redacting signed download queries. 14 scenarios are recorded on dev and 11 are derived from them: retries, proxy refusals, an unknown part status and enum value, a renamed strict field, and place choices in one and two parts.
The validator replays each record's steps over its exchanges and fails when expect differs from the outcome the bodies and actions.json imply: error class, gap kinds, option and candidate counts, and a dataset's members, complete and missing. The capture tool imports the same rules. The validator also tests a strict member's nullability with the schema validator itself, refuses an absolute URL or Location header that keeps its query, and lets a synthetic record name its parent's body file. The capture refuses untracked files and an environment out of step with the lock. door_not_implemented now reports and method_not_allowed upgrades.
Nineteen scenarios, four recorded on dev and fifteen derived from recorded parents, cover a timed-out and an unsupported part, an unknown answer kind, every choice refusal, a choice named by a mention, a several-part candidates choice, a candidates part left alone beside a chosen one, a placeholder for a part with no restatement, and the SearchWarning rule. An unchosen candidates part posts its candidate set in the server's order. The validator checks every Search request a record holds against the input the rules derive, and pins the part column of a stacked dataset.
client-codes.json lists each code a client raises when no problem document carries one, with the error class it raises. The codes are disjoint from actions.json and the codes registry, and every scenario error that no refusal in its exchanges carries names one of its class. record --offline re-derives a recorded scenario's expect from its own exchanges.
…rios Ten scenarios cover a frame-layout build, a 504 twice, an unresolvable member, a strict data warning, a digest mismatch, a resubmit answering another spec, a frame fetch of several readings, and an expired ref that rebuilds the same version, lands on a newer one, or cannot be rebuilt. Six more are defined and wait for a served document that declares the build's 202 or the file selector. The validator derives poll waits, resubmissions, the operation id and the newest version, and a refusal with no known code maps 408 to DfoTimeoutError and 410 to NotFoundError.
The server's scope replaces the spec's entity binding whole, so a client starts from the spec's bound roles, replaces the roles the caller names, drops a role bound to null, and posts the whole map. The validator checks every build's input.scope against that rule, and two dev recordings rebind and drop the origin of a two-role spec.
The identity entry a client posts to name a built spec carries the spec's entity_groups for each role the build binds to the same entities as the spec's definition. A window or facets narrowing keeps every group, and a role the narrowing rebound or dropped loses its group, so the sentence names "the 44 Least Developed Countries" where the group still holds and lists recipients where it does not. validate_client_contract.py derives the kept groups from the held spec and checks every posted identity entry against them. Two new recordings cover a narrowed build and a rebound origin, bringing the contract to 58 scenarios.
Every recorded scenario and the served OpenAPI document now come from
one dfo-apis commit, 635943e03, which composes dev with the search
choice options, answer statements and datasets client contract
branches. The served document lists the four deployed surfaces:
Search, Datasets, Graph and Presentation. The contract holds 67
scenarios, 29 recorded and 38 synthetic.
The long builds record against the composed app: mint-async-build,
operation-resume and their synthetic children, and the expired file
redirected through the file selector. Signed download URLs read as
storage.googleapis.com URLs with the signature redacted, since a
production download is cross-origin; origin.base_url states the base
URL the client was given.
The rules the recording pins:
- Every candidate pick, in a one-part answer too, posts the reading's
identity to Search as a dataset_resubmit entry echoing
reading_assumptions[spec_id] as its assumptions, and the picked
answer and its sentence come from Search's response.
- A client asks for the newest stored version with
GET /datasets/{id}/versions?limit=1; only not-found reads as none.
- A mint takes only a layout.
- A required member that admits null must be present.
- capability_unavailable keeps the fix_request action, pinned by
fetch-spec-capability-unavailable.
The validator now refuses what a client replay would reject. A repeat of a refused request counts as a retry only while the refusal is retryable and the budget allows: two retries, one for a build cut off by a 504. A POST record carries its body, and every build's posted body is compared whole with the spec it builds. A poll names the operation the client holds, a download follows the files route's Location exactly, and synthetic exchanges go through the same problem code check as recorded ones. served/openapi.json must hold the Search, Datasets, Graph and Presentation surfaces and nothing else. A recorded scenario holds only exchanges the server made: the capture refuses to write one with a canned answer, and no synthetic derivation can make a request. choose-option-beside-definite and fetch-ref-newest-version are recorded at 635943e03, and fetch-ref-expired-not-reproducible derives from the latter through a new newest_version step. The README states every rule the validator derives, including the retry, warning, long-build and choice refusal rules. 68 scenarios, 31 recorded and 37 synthetic.
…hapes
Outcomes gain retry_waits, the seconds before each retry, and
warnings, the codes API responses issued. A retry waits Retry-After,
else 0.5 s doubling to 8 s, and a record that ends with retries left
is unfit. The files route follows the ordinary retry rules; a failed
signed fetch gets one more round, and a second network failure raises
as itself. A relative Location resolves against the files route.
New tables: proxy-codes.json for the website proxy's error codes and
warning-codes.json for the warnings a client issues itself, each with
its schema. client-codes.json rows state retryable, and key_empty
joins them. The validator compares the whole Search body, checks that
recorded commits match served/SOURCE.json, refuses any absolute URL
off the storage host, and reads facets: {} and entities: {} as no
narrowing.
New scenarios pin a signed URL refused twice, a relative Location, a
failed operation with no error, an unreadable versions page, a 504
carrying Retry-After, a Sunset-only warning, a part status that is not
a string, a definition naming another spec_id, an option without a
data_query, a Search response with no JSON body, and an empty
narrowing. Steps no longer take timeout; the README states the
clients' timeouts. 79 scenarios, 31 recorded and 48 synthetic.
…usal and choice rules A manifest artifact's url must sit on the API's origin, relative or absolute; any other origin, the storage host's included, raises contract_violation before any request. Only a files route's Location may sit on the storage host. The strict check walks down from the body's root through strict members alone, so a spec nested where no rule reads it raises nothing. Spec.resolved, SpecDefinition.identity and DefinitionModel.identity join strict-fields.json, which now holds 28 required pointers. A resolution with empty parts raises contract_violation at /parts. A build's POST shares the two retries, and after a 504 cut-off it is posted once more only: 504 then 503 raises the 503. A refused poll changes no pacing, and a non-retryable one raises at once. A problem document without a code keeps its retryable and trace_id. A 401 resolves the key again. request() refuses a path that is not a path on the base URL. A one-part option whose data_query holds other than one part raises ContractError in a one-part answer too, and the validator derives it. 22 new synthetic scenarios pin these rules, the header warnings, the separate and operations echo, the unsupported placeholder and three choice_invalid cases. 101 scenarios, 31 recorded and 70 synthetic.
Every error outcome carries trace_id. An error that describes a response carries that response's trace id: the body's trace_id when the body is an object holding one, else the x-dfo-trace-id header, else null. Refusals, failed operations, VersionUnavailableError, unexpected_response, contract_violation on a body, answer_declined, answer_incomplete and a strict SearchWarning or DataWarning describe a response. An error that describes the caller's input, such as choice_invalid, invalid_argument or a key-lookup reason, and one raised on a storage-host response carry null. The capture tool keeps x-dfo-trace-id on recorded responses and writes trace_id on every error outcome. search-refused-without-code pins the body over the header, search-unauthorized the header on a bodiless refusal, answer-timed-out an incomplete answer taking the Search response's header, and choose-refused-shared-label a choice_invalid that stays null beside one.
…nd pin malformed members Every recorded scenario and served/openapi.json are recorded again at dfo-apis 635943e03, now keeping x-dfo-trace-id; the capture refuses an API response without it unless the response is Cache-Control: public, which the server sends untraced. The served document is unchanged. The validator replays each scenario under its recorded base URL and under https://replay.example.org with the recorded origin replaced in bodies and headers, so a record that holds only under its own host fails. A refused poll waits its Retry-After whatever its status, else the ordinary pacing. The trace id is a body's top-level trace_id, else the header; diagnostics.trace_id is not read. A signed fetch answering anything but 200 fails its round, and a files route answering a 2xx other than 200 or 302 raises unexpected_response. A strict member of the wrong JSON type raises contract_violation, and a non-strict list member of another type reads as absent. A ref member the snapshot manifest does not list takes its rendering from the ref. The README states the stacking, part column, variable_text and label rules, and the errors that carry no trace id. 108 scenarios, 31 recorded and 77 synthetic.
… and pin the files route and 429 polls The validator reads every non-strict member through one set of typed accessors, so a member or item of another JSON type reads as absent and no malformed body crashes it. A strict list's items must be objects, and one of another type raises contract_violation at its index pointer. A files route answers 302, which the client follows, or 200, whose body is the file; any other status but a refusal raises unexpected_response. A 429 poll whose problem document says retryable false raises at once. A definite part whose variable_text is absent or empty adds nothing to its member's parts. The validator reports a recorded API response without x-dfo-trace-id unless it is Cache-Control: public, and a body trace_id must be a non-empty string. New scenarios pin a 307 and a 200 from the files route, a non-retryable 429 poll and folded parts without names. 112 scenarios, 31 recorded and 81 synthetic.
…s, and report a crashing derivation against its record kept_entity_groups reads entity_groups, the identity and its entities through read_object and compares only the string refs a role binds, so a malformed binding reads as absent. An exception while deriving one record's outcomes is reported against that record, and the other records are still checked. A body trace_id that is empty or not a string reads as absent, and a test pins it. The scenario schema's headers description names the cache-control header that excuses a shared-cacheable response from x-dfo-trace-id.
…rose Split clause-joining semicolons and mid-sentence colons into sentences and state facts positively in the package README, the rules README, tools/README.md and the capture and validate tools. Every rule keeps its cases and codes; the tools keep their help text. No code changes.
Describe the API gate as schema and registry validation of recorded exchanges. Scope disk-cache rules to clients implementing that capability and state the synchronous platform DNS timeout exception.
Distinguish content-addressed byte entries from replaceable manifests. Concurrent manifest writes can reduce cache availability while artifact digest verification continues to protect data identity.
…covery gaps Capture and the validator refuse a symlinked or out-of-directory body file before reading or writing it. A ref's `definition` requires the `schema_version` and `identity` the served `CanonicalDefinition` needs, with three rejected examples. The resubmit echo posts the object items of `operations` and `assumptions` alone, a candidate may be chosen by its label as well as its index, and a rebuild that answers another `spec_id` raises `contract_violation`. The README states the browser redirect rule the TypeScript client follows. Five synthetic scenarios pin the new rules (117 in all).
An `assumptions` or `reading_assumptions` item now reads only when its `kind`, `binding_ref` and `text` are strings, and an `operations` item only when its `verb` and `phrase` are strings, matching the served `Assumption` and `Operation` schemas. A read item holds its schema's members alone, with wrong-typed optional members absent. The resubmit echo posts exactly the items read, built from the members read, so a client never posts back an item or member the server's schema refuses. `choose-echo-drops-malformed-items` pins a wrong-typed required member, wrong-typed optional members, a filtered `refs` list, an unknown member and an assumptions list that posts `[]`.
JSON Schema's `integer` admits `2020.0`, and a JavaScript client cannot tell it from `2020` after parsing, so an operation's `target_year` now reads when it is a number with no fractional part other than a boolean, as that integer. `2020.0` reads and echoes as `2020`; `2020.5` reads as absent.
A manifest definition's `schema_version` is now a strict required member, so a client refuses a manifest without one (pinned by `fetch-spec-manifest-without-schema-version`) and never writes a ref it would then reject. The ref format takes any integer version and leaves it to the server, and its definition admits no member CanonicalDefinition does not (`ref-definition-extra-member-rejected`). The README states that the echo's item rule checks JSON types alone and the server still checks its value constraints, that an operation that does not read gives no "Not done" hint, and that a boolean option is neither an index nor a label. The oracle quotes hint phrases keeping non-ASCII characters, as both clients do, and refuses a boolean option. The capture and the validator share one `stored_version` decision for ref recovery. `BODY_FILE_NAME` is compiled from the scenario schema, and a refused body file is checked once, on read, with a message naming it.
…boolean part An integer is a JSON number with no fractional part that is not a boolean, as JSON Schema reads it, since a JavaScript client cannot tell `1.0` from `1`. Strict integer members, a manifest definition's `schema_version` among them, and a ref's `definition.schema_version` admit `1.0` and refuse `1.5` and `true`. A choice that names its part by a boolean raises `choice_invalid`, as a boolean option does.
…-contract feat(client-contract): add the contract the DFO clients follow
… dfo-apis 035d29cd0 (#15) * feat(client-contract): read the newest plain build and record against dfo-apis c7543c9d6 "Newest version" is the first listed version with no base_content_version, read across every versions page (`validate_client_contract.newest_plain`), so a derived groupby rendering listed first is skipped. The served document and all 31 recorded scenarios are captured at dfo-apis c7543c9d6, whose versions page carries `enrichment_stamp`, and the 87 synthetic scenarios are derived again. Twelve scenario questions are reworded where Search now answers the old wording with another shape: the candidates questions read mortality in Kenya, the async build reads ODA disbursements by sector, and the place question names a year. * fix(client-contract): record at dfo-apis 035d29cd0 within a size budget and check refs against their scenarios The async build questions read one variable over donor and recipient groups, so the finished operation's manifest lists one variable and the scenarios hold 13,080,568 bytes, under the 16,000,000-byte budget the validator and the record run now enforce (`validate_client_contract.SCENARIOS_BUDGET`). A ref whose record holds members names the scenario that serves them, and the validator checks each member's spec, content version, sentence and digest against it (`_served_by`). `newest_plain` reads the whole versions page before it takes a plain version. The validator tests hard-link their package copy.
…e URLs, transport and oversized bodies (#16) key_invalid, credentials_file_unreadable, header_invalid, insecure_transport, base_url_invalid and response_too_large let the clients refuse an unsafe key, header value or base URL, a key over plain http to a remote host, an unreadable credentials file and a decoded body past its cap, each under its own code. None is retryable.
… rendering (#17) dataset-ref.schema.json closes the rendering selector with additionalProperties false, so a ref to a version minted with without_party_totals could not be written by either client. The selector now takes without_party_totals: true beside layout. The key is written only when true, so a plain ref keeps its bytes and false is refused. ref-without-party-totals is the accepted example and ref-without-party-totals-false-rejected pins false.
A ref names one rendering's digest. When the server renders the same content_version again, the file a client downloads hashes differently, and the client returns the data. warning-codes.json gains ref_digest_changed, a DfoWarning, so both clients issue that warning with a code the contract lists, and the validator requires the row.
Search gives a part its request deadline cut short a no_data reason of its own. The registry and actions.json gain timed_out, with action ask_data_question and gap kind timed_out, which actions.schema.json now admits. The name keeps clear of the client code timeout, a transport timeout with action report.
…e-record at apis 0ba344270 (#20) The website proxy's own refusals (rate_limited at 429, transient_unavailable at 502) are application/problem+json documents with code, retryable and trace_id, as every other refusal is. The README, proxy-codes.json, client-codes.json, the scenario schema, the validator and the capture tool drop the {error, retry, detail} body. The validator checks a proxy problem document's media type, members and retryable, and refuses a 202 whose operation reads done. A 202 operation reads pending or running, or failed for a deterministic failure. fetch-spec-accepted-running pins running, and fetch-spec-poll-pacing-changes pins a retry_after_ms that changes from one poll to the next. served/openapi.json is re-captured from apis 0ba344270, which adds the timed_out no-data reason and the optional entity_refs member, and the 31 recorded scenarios are re-recorded live against dev at that commit. Two validator nits: a wrapped docstring line and the test id action-row-no-data-reason-missing.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Releases
devat0d19172tomain: thepython/client-contractdistribution (#11, #15 to #20), registry changes (#9, #10,whole_total_available), and the move to the DevelopmentFinanceObservatory org.The registry goes from 199 to 205 active codes:
slot_default_not_applied,source_code_ambiguous,source_code_not_held,source_code_shares_variable,timed_outandwhole_total_available. dfo-apis pins0d19172on bothdevandmain, and dfo-ai pins708f293, whichmainalready holds, so no consumer changes. Merge with a merge commit, so0d19172stays reachable frommain.