Skip to content

release: spec-kit dev to main - #22

Merged
jm-rivera merged 44 commits into
mainfrom
dev
Oct 25, 2026
Merged

jm-rivera merged 44 commits into
mainfrom
dev

Conversation

@jm-rivera

Copy link
Copy Markdown
Contributor

Releases dev at 0d19172 to main: the python/client-contract distribution (#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_out and whole_total_available. dfo-apis pins 0d19172 on both dev and main, and dfo-ai pins 708f293, which main already holds, so no consumer changes. Merge with a merge commit, so 0d19172 stays reachable from main.

dfo-release Bot and others added 30 commits October 13, 2026 07:10
…f323ba6c7f

Synced-from: ONEcampaign/dfo-spec-kit@5d381d8322288dc77411baa70c0dcff323ba6c7f
The $id values in the schemas stay as they are. The sync-guard workflow is
removed.
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.
@jm-rivera
jm-rivera merged commit 3361afd into main Oct 25, 2026
38 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant