All notable changes to the dfo-spec-kit-client-contract Python package will be documented in this file. The format is based on Keep a Changelog, and this package adheres to Semantic Versioning.
- Package scaffolding (uv, ruff, ty, pytest).
served/openapi.json, the composed dfo-apis app's OpenAPI document, andserved/SOURCE.json, naming dfo-apis commitd80df9f5fd598b7345255470932d202ee5d49942. The document admitstimed_outas a no_data reason and the optionalentity_refson each answer part, a list ofEntityRefswith one entry per restatement entity row, the graph nodes that row binds, and a stored manifest warning states its counts and the refs it names (CountedRefsModel,CountedEntityYearsModel,EntityYearsModel).NoDataOptionandSentenceTokencarryentity_choice, the index intoNoData.ambiguous_entitiesof the group an entity_ambiguous option settles or a slot token asks, so a client groups a place menu by it when two wordings share one text.strict-fields.jsonand its schema: 29 members that stay required and non-nullable, each one a client reads, and the answer kinds, part statuses and operation statuses the clients branch on.actions.jsonand its schema: the caller's next step for each of the 106 active refusal codes onproblem_detailsorno_data_reason, with the gap kind of each no_data reason.- The status fallback for a refusal whose code has no row: 400 and 422 to
InvalidRequestError, 401 toAuthenticationError, 403 toPermissionDeniedError, 404 and 410 toNotFoundError, 408 toDfoTimeoutError, 429 toRateLimitError, and any other 4xx or 5xx toInvalidRequestErrororServerError. scenario.schema.json, the scenario record, and 126 scenarios: 34 recorded against the deployed dfo-apis dev app atd80df9f5f, and 92 synthetic, each derived from a recorded one by a one-line edit, with 21 binary bodies totalling 1,134,603 bytes and records and bodies together 13,695,441 bytes.search-place-ambiguous-twicepins two entity_ambiguous groups of one text, each option and slot token naming its group byentity_choice, andchoose-refused-label-in-two-groupspins that a label both groups offer matches two options, so a choice there names its option by index. The scenarios cover the several-part choice entries and the assumptions each identity entry echoes, a picked reading whose assumption Search states in its answer, the place choice through a no_data part's options, the choice refusals, a part without variable_text, an unknown status without a restatement, a shared label, a one-part option whose data_query holds two parts, the separate and operations echo, an unsupported part's placeholder, the timed_out, unsupported and unknown gaps, an unknown answer kind, theSearchWarningrule, the stacked dataset'spartcolumn, the frame layout, a strict fetch'sDataWarning, a spec fetch that rebinds one of two entity roles and keeps the other, one that drops a role bound to null, a spec whose destination is a named group fetched with a window or with its origin rebound, whose sentence lookup posts the groups of the roles the build left bound, a twice cut-off build, a cut-off build retried after its Retry-After, a retryable refusal whose Retry-After passes the 60-second cap, a build refused 504 then 503 and one refused 503 then 504, a refused build, a build refused 501capability_unavailable, a digest mismatch, a signed URL refused on both rounds, a relative files-route Location, an artifact url on a foreign origin, on the storage host and absolute on the API's origin, a manifest definition without identity or schema_version, a Search spec without identity or resolved, nested specs no rule reads, an empty parts list, a Search body that is not an object, a codeless 503 problem document that is not retryable, a 401, an identity resubmit answering another spec, a part whose status is not a string, a Search spec whose definition names another spec_id, a Search response with no JSON body, an option with no data_query, empty entities and facets, a Sunset header issued once,x-dfo-warningmessages and a Deprecation and Sunset pair, an unreadable versions page, a mint and a spec fetch whose build answers 202 and is polled to done, a resumed operation, a rate-limited poll, 503 polls retried within the budget an answered poll restores, a 202 that reads running, polls whoseretry_after_mschanges from one to the next, a 404 poll that raises, a failed operation posted again, a failed operation with no error, and a ref whose stored file expired, read through the file selector, rebuilt at the same version, at a newer one, or refused with the newest version read from the versions route: the first version it lists with nobase_content_version, the newest plain build, so a derived version listed before it is skipped.client-codes.jsonand its schema: the 23 codes a client sets on the errors it raises itself, each with its error class, retryable flag and condition, disjoint fromactions.jsonandregistries/codes.json, and theclient_codes()andclient_codes_schema()accessors. Every scenario error the client raises itself carries its code, andrefusal_without_codenames a refusal whose body carries no code.proxy-codes.jsonand its schema, the website proxy'srate_limitedandtransient_unavailablecodes with their statuses, which the proxy sends in problem documents of the shape every refusal has, both retryable, withRetry-Afteron the 429, so a client reads one refusal shape, andwarning-codes.jsonand its schema, the 7 codes a client sets on the warnings it issues itself, with theproxy_codes()andwarning_codes()accessors.ref_digest_changedinwarning-codes.json, aDfoWarningfor a ref member whosecontent_versionserves a file with another digest than the ref's, because the server rendered the same data again. The client returns the data, with no error under strict.timed_outinactions.jsonandregistries/codes.json, the no_data reason Search gives a part it left unresolved at the request's time limit, with actionask_data_questionand gap kindtimed_out, whichactions.schema.jsonnow admits. The client-sidetimeoutcode, a transport timeout, is unchanged.- A no_data part's gap kind comes from its own reason, whatever the answer's
diagnostics.timed_outsays.answer-timed-outgives its part the reasontimed_out, andanswer-timed-out-keeps-reasonspins a part that keepsvariable_not_resolvedas anot_understoodgap in a timed-out answer. - Error outcomes give the error's
trace_id, the body's, else thex-dfo-trace-idheader's, else null. Scenario outcomes giveretry_waits, the waits before each retry, andwarnings, the warning codes a step's responses issued. Scenario steps take notimeout. dataset-ref.schema.json, the frozenDatasetRefformat, with accepted and rejected examples.without_party_totalsin the ref's artifact rendering,truefor a member fetched with party totals left out and absent for a plain rendering, withref-without-party-totals, a default fetch's cut rendering,ref-one-member, a fetch with party totals and its plain rendering, andref-without-party-totals-false-rejectedas its examples. Each ref example's rendering is the one the build that answered its file posted, and the validator checks it.tools/capture_client_contract.py, which capturesserved/openapi.jsonfrom a dfo-apis checkout in process and records the scenarios against a base URL, dev by default, or withrecord --offlinederives their outcomes again from the records with no request. A local app's signed URLs are written as the storage URLs a deployment signs.recordreads the rule tables from--package-dirbefore any request, and looks for each long build's spec from the year after the one its record built, stopping at the first start year whose estimate is within the sync budget.tools/validate_client_contract.py, which checks the contract against its schemas, the served document andregistries/codes.json, checksclient-codes.jsonagainst them and against the reasons the key-lookup rule raises, and checks each scenario'sexpectagainst the outcomes its exchanges,actions.jsonandclient-codes.jsongive, each Search request against the input its steps derive, a sentence lookup'sentity_groupsincluded, each build'sinput.scopeagainst the narrowing its step names over the spec's bound roles, each build's options against its layout, each Search request's whole body, each download against its artifact's digest, each recorded scenario's commit againstserved/SOURCE.json, each problem document's code againstregistries/codes.jsonor, for the website proxy's,proxy-codes.json, each proxy problem document's content type, members andretryable: true, each 202 against an operation that readsdone, and each absolute URL against the storage host, each ref example's members against the dataset members and downloaded files of the scenario it names, and the scenarios directory against a 16,000,000-byte budget for records and bodies together. A record that ends a run of retries while a retry remains does not fit. The outcomes include each long build's operation id, poll waits and resubmissions, and aVersionUnavailableError's newest version.- The contract README states that a POST for a build identical to one pending or running returns that operation, so a client that stops waiting posts the same build to join it, and that
fetchwaits for a build with no whole-call deadline by default, with a download that delivers no byte for 30 s a retryablenetwork_error. - A fetch of an answer or a spec leaves out party totals by default: its build posts
without_party_totals: trueinoptions, so the server drops the group totals of the identity's open party roles, and a role the caller binds keeps what it binds. Fetch steps takeparty_totals: truefor the client option,party_totalsin Python andpartyTotalsin TypeScript, false by default, which posts nowithout_party_totals. The wire flag and the ref format are unchanged, and a ref recordswithout_party_totals: truefor a member fetched with the default.fetch-spec-party-totals-left-outandfetch-spec-party-totals-keptpin both on DAC2a ODA to Kenya in 2022 with the origin open. A mint leaves them out by default too, and its step takesparty_totals, so itsresultanswers the table a fetch of the spec answers;mint-async-buildkeeps them. A resumed operation'sresulttakes the rendering its build posted from the done manifest's artifact. - The
reading-orderrule and its vectors, which carry rows: a client presents a dataset's columns with the reading columns first, the time period, provider, recipient, subject, purpose, value, unit, flow type and provenance, each slot filled by the long layout's label or the frame layout's code, the flow type by CRScrsTypeOfFlowor else DAC2adac2aAidType, then the columns whose values differ between rows, then those that hold one value, each group in the server's order, withstat_varlast. It sorts each member's rows by the filled slots withoutvalueand the time period, then the varying columns, then the time period, ascending with nulls last and ties in served order, so each series' years read together. Two cells hold the same value when they are equal as JSON values: numbers by value, so1and1.0are one value, booleans apart from numbers, NaN the same as NaN, lists item by item in order and structs field by field in any order. A column of lists or structs is no sort key. A vector's rows hold one cell per column and each column one JSON type, andinteger-and-float-are-one-valueandnested-cells-compare-by-valuepin the comparison. Scenario outcomes givecolumnsin that order, and the validator checks it against the served files, list and struct columns included, and reports a malformed vector. - The
client-identityrule and its vectors: every request a client sends to the API carriesX-DFO-Client: <client>/<version> (<runtime>), such asdfo-python/0.1.0 (CPython 3.14)ordfo-ts/0.1.0 (Node 22.18),browseras the runtime in a browser page or browser worker, andotherin any other runtime, such as Cloudflare Workers, withpatternmatching every well-formed value. A request to another origin, a signed storage URL included, carries none. A caller's headers or header provider namingX-DFO-Clientin any letter case raisesInvalidRequestErrorheader_invalid, whose condition inclient-codes.jsonnames that case, andUser-Agentstays as each client sends it. A scenario replay checks the header on eachpathandorigin_pathrequest and its absence on eachurlrequest. - The
role-keysrule and its vectors: a caller narrows a spec withentities=keyed byplace,originanddestination, and a client postsplaceas the wire roleobservationAbout.Spec.roles,specRoles(spec)and the clients' errors name the caller keys, and a flow spec takes both directions. A key outside the spec's roles raisesInvalidRequestErrorinvalid_argumentnaming the key and the spec's roles, soobservationAbouton GDP is refused namingplaceandfinancingRecipienton a flow spec namingoriginanddestination. A spec that names no role postsplaceasobservationAboutand other keys as typed, and refusesobservationAboutnamingplace. - The client rules and their vectors: the credentials path and file format, the key lookup order, with an empty
api_keyrefused askey_emptyand an empty profile key asprofile_without_key, the key format check, the base URL order,base_url_invalidfor an invalid value, and the cache directory and layout. - The
base-urlrule states the whole validity rule, and seven new vectors pin it: no query, fragment,.or..segment, character outside visible ASCII, empty userinfo or non-decimal IPv4 host; trailing slashes dropped; a relative URL valid only in a TypeScript client inside a browser page. - The
base-urlrule also refuses a.or..segment written with%2e, a backslash, and a port outside 1 to 65535 or written other than in decimal digits, an empty one included, so a client on Python'surlsplitand one on the WHATWG URL parser accept the same values. Ten vectors pin them. - The
file-selectorrule and its vectors: the file selector's query holds theformat,bundleandlayoutof a ref member's rendering, the keysquery_keyslists, in that order, and leaves outwithout_party_totals, which the selector refuses with 400request_invalidbecause the version records its own cut.fetch-ref-expired-file-selectorandfetch-ref-artifact-unlistedask that query, and the validator and the capture build it fromquery_keys. - Ref format 2 in
dataset-ref.schema.json:format_version: 2and one or more derived members, each{parts, sentence, spec: {spec_id, recipe: {normal_form_hash, replay_key}}, content_version, output_digest, roots, execution_identity, resolved_periods, artifact}, with anops_id, anocv_version,rnf_andrpk_recipe hashes, anout_output digest, the root dataset versions with the renderings they were read with, and each period stated relative to the latest with the year it resolved to. Format 1 is unchanged and holds dataset members only, and format 2 holds no dataset member. A derived member'spartsis empty. A scenario's fetch-by-ref step admits a format 1 ref only, and a format 2 ref fails there on itsformat_version. Nine new ref examples, two accepted and seven rejected, each derived identity computed by the dfo-apis analysis runner. The validator checks a derived member's roots against the scenario it names, each root's rendering as the posted one in the frame layout, and that the derived members of the ref examples and theoutput-comparisonvectors agree wherever one identity fixes another. - A derived member's root records its rendering in
root_rendering, the artifact selector's members andgroupby, the sum DFO read the root with asoptions.groupbystates it:by, one or more distinct column names,opsum, andlevels, 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'srenderingadmits nogroupby.ref-derived-grouped-rootis accepted, itsbysorted 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, andref-derived-root-groupby-op-rejectedandref-artifact-groupby-rejectedare rejected. The validator checks a summed root's rendering against its scenario's build without itsgroupby, 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, theout2digest of a derived frame's values with thestat_var, members, period and unit each belongs to, withoutmeasurementMethod,provenance,statusor a unit'srecipe, reading the variablecompare,sum_of,share_oforquotient_ofwrites under anmv_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 aquotient_ofover asum_ofby the inner step's name;output-comparison, how==andsame_datacompare two refs, with 12 vectors;operation-outputs,share_ofin per cent underPercent,joinin long format bystat_var,compareas data minus other,sum_ofas data plus other, andgrowthas a fraction invaluewith 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 withrole_ambiguous, with 10 vectors;rolling-composition, one latest period per recipe tree, refusing rolling steps over different populations withrolling_populations_differeven when they settle on one year, with 6 vectors; andpublication, a share's numbered slots that keep their URLs across a live replace, a snapshot that refusesshare_not_replaceable, and the write token checks, with 8 vectors. actions.jsonrows forallowance_exhausted(wait_for_reset),rolling_populations_differ,several_members,share_not_replaceableandwrite_token_required(fix_request), andwrite_token_invalid(stop).- The
stopaction, classPermissionDeniedError, 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, andprovenance_unavailable, since a recipe root that cannot be cited survives a rebuild. - The retry rules: a refusal whose code's action is
wait_for_resetis never retried and raisesQuotaExceededErrorat once, whatever its document and status say, and a refusal whoseRetry-Afteris longer than 60 seconds (RETRY_AFTER_MAX_S) raises at once with that wait, a refused poll included.search-retry-after-past-cappins the cap. series_left_outinwarning-codes.json, classSeriesLeftOutWarning, and theseries-left-outrule with 8 vectors: a fetch of derived members issues it once when any member's derived manifest lists left-out series inleft_outor saysleft_out_completefalse, a nullleft_outissues nothing, and a strict fetch raises it in place of its data.tools/validate_client_contract.pytakes the code from the rule.actions.jsonrows (fix_request) for the 34 refusal codes the analysis engine serves ondatasets.analysis, includingcompare_no_key_matchedandsum_of_no_key_matched.actions.jsonrows (stop) for the two refusals of the exports door,provenance_unavailableandlicence_not_redistributable.tools/validate_client_contract.pylets a proxy code share a registry code whose every mapping ismcp_refusal, asrate_limitedandtransient_unavailabledo.