Skip to content

refactor(search): take API and graph types from the generated client package - #43

Merged
jm-rivera merged 25 commits into
devfrom
feat/client-generated-types
Oct 23, 2026
Merged

jm-rivera merged 25 commits into
devfrom
feat/client-generated-types

Conversation

@jm-rivera

@jm-rivera jm-rivera commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Stacks on #42.

The website takes its API and graph types from @developmentfinance/client, vendored as vendor/developmentfinance-client-0.1.0.tgz and built from DevelopmentFinanceObservatory/sdk at 3d8bb48 (DevelopmentFinanceObservatory/sdk#2). src/lib/search/types.ts holds only the page's own types and the stream payloads the OpenAPI document does not describe, and this removes the hand-mirrored wire types, scripts/graph-wire.mts, src/lib/graph/schema.d.ts and src/lib/graph/graph-api-schema.json. Specs, slot options, assumptions, stream entries and observation members read as the generated wire types, and an open enum keeps any value a newer server sends.

Typing against the wire changed some behaviour, and each change needs a reviewer's eye. renderingArtifact in snapshot.ts skips layout and without_party_totals renderings, and a null member reads as absent. storedDownload returns the file's digest. handoffSeeds drops a verb it does not know. Unknown verbs, roles and gates get fallback text. A shared edit skips a spec with no slots (sharedEdit.ts). A spec flattenSpec cannot read counts as 0 stat vars in searchEvents.ts. NO_DATA_REASON_TEXT in view.ts holds the copy for each no-data reason the client types name, and that copy needs an editor's read.

docs/development.md describes how to vendor a new package version.

Testing: typecheck, lint, the 1,612 tests outside db/ and pnpm build pass.

…changed

The Search API now lists each part's assumptions and closes the sentence with an "Assumed: ..." clause in `assumption` tokens. The answer block draws that clause as its own paragraph after the departures, and "About this answer" states each assumption from `part.assumptions` in place of the place-scope and role-scope warnings, so each is said once. A dataset_resubmit identity entry echoes the part's assumptions back unchanged, so an edit keeps the defaults it did not touch.

`no_data.options` replaces `no_data.data_query`, and an entity pick posts the matching option's request. `source_code_not_held`, `source_code_ambiguous` and `source_code_shares_variable` get their own messages.
An empty list tells the engine the previous answer stated no assumptions, so the edited answer states none the echo lacks. Only a response without the field leaves the echo off.
…part sentence

A sentence written part by part labels each byline with its part, so its clauses take the same label even when only one part states a clause; otherwise that clause reads as covering the whole answer.
… by their lead, and keep a share's assumptions

The entity picker mapped a picked candidate to its option by dcid. A group candidate on a members row posts the group's registered name with no dcid, so the match failed and the fallback pinned the group with `members: true`, a request the API refuses. Both pickers (`SearchCards.tsx`, `SearchSentence.tsx`) now list `no_data.options` grouped by their mentions (`entityChoiceGroups`) and post the chosen option's `data_query` part unchanged; the restatement rewrite is gone. A parsed entry keeps the operations it was built with.

`assumptionClauses` labels a clause by the owner of its lead token whenever the answer has more than one part, so a definite part beside a candidates or no-data part names its clause, and a clause every folded part shares carries no single part's name.

A share replays through `dataset_resubmit`, where a missing echo states no declared default, so a shared ODA answer lost "official development assistance". `share_ref` keeps each part's `assumptions` (migration `0018_share_ref_assumptions.sql`), and replay echoes them; a share stored before the migration replays with none.
`isAssumption` checked an echoed assumption's shape but none of Search's bounds, so a share could store a text past 200 code points, two refs or 68 items, and every replay of it failed with `request_invalid`. The share request now applies Search's bounds on `Assumption` and on a part's list, as it does for `EntityGroup`.
…ing's on a candidates entry

A candidates part's `assumptions` holds only what its readings share, and a pick echoed that list, so the picked answer dropped the reading's own open role or place scope. A pick (`candidatePickEntry`) now echoes `reading_assumptions[spec_id]`, a candidates entry echoes each reading's list on its candidate, and a reading the response gives no list for posts no echo. Only a definite part's identity entry echoes the part's own list: an edit of a no_data part reads fresh. `types.ts` mirrors `CandidatesPart.reading_assumptions` and `ResubmitCandidate.assumptions`, and the `AssumptionKind` comment uses the API's wording for an open role.
A permalink rebuilt each identity entry from four fields and dropped its `assumptions`, so a reloaded candidates pick, which echoes its reading's own list, replayed with no echo and stated no declared default. `replayableEntry` keeps the echo, `[]` included, when it passes the share request's validator (`isAssumptions`, now shared), and drops an echo Search would refuse.
A permalink replay posts each stored edit with the echo it was posted with. `withAssumptions` overwrote that echo with the fresh part's list, so an edit that had narrowed the list (a flow set back to ODA after an edit to OOF posts `[]`) replayed with the default stated again. An identity entry that already carries `assumptions` now keeps them; a live edit's entry carries none and still takes its part's list.
Search sends "Answered N of M parts." as tokens of its own `coverage` segment (DevelopmentFinanceObservatory/apis `feat/search-answer-statements`). The answer block draws it as a muted lead above the `<h1>`, beside "Closest match", and `planSentenceText` reads heading tokens only, so the heading's accessible name, the run label and the Studio hand-off question no longer open with the coverage line. `planCoverageText` reads it.
…ut this answer

Search sends an open destination role as binding_ref "where:", the slot the destination shares with the subject. assumptionSentence branched on "destination:", so the sentence dropped the provider that limits the scope ("covers every recipient" for "covers every recipient from Germany"). The why and resultNotes fixtures now carry the served ref.
…faulted slots from the About summary comments
The site depends on @developmentfinance/client through a vendored
tarball and reads every API body it uses from the package's generated
wire types, so src/lib/search/types.ts derives its shapes from the
served OpenAPI document. Parts narrow through isKnownPart before any
payload read, optional wire members default where the site reads them,
and open enums fall back where the site maps every known value. A
stored rendering cut with a layout or without party totals no longer
counts as the default file.
src/lib/graph/wire.ts re-exports the graph types from
@developmentfinance/client, whose served document covers the Graph
surface. The second generator goes: schema.d.ts, graph-api-schema.json,
scripts/graph-wire.mts and its test, the graph:types and graph:schema
scripts and recipes, and openapi-typescript.
flattenSpec takes the generated Spec and reads resolved.trace; the
resolved.resolution fallback read a field the wire lacks and is gone.
Slot options are the generated SlotOption, the share's assumptions
column is typed as the wire's Assumption list, and ASSUMPTION_KINDS
is a Record over the generated kind union, so a new kind fails the
type check.

Two fixes ride along. A shared answer whose assumption omits refs,
which the wire allows, is accepted. A sentence token with no segment
reads as heading in readingsSummary, as it does in sentenceBlocks,
through the shared tokenSegment.
… types

The stream payloads keep their hand-written envelope, since the OpenAPI
document does not describe the SSE stream, and every wire member inside
them takes its generated type: slot options, sentence slots,
observations, facet and entity options, and the normalised response.
SlotOption and ObservationPoint reads go through the generated types,
and normalizeResponse returns the page ResolveResponse; tests read
fixtures through resolution(), which narrows on kind.

Every reader treats a token with no segment as heading through
tokenSegment in sentencePlan.ts, and a segment the site does not know
stays its own block, which no reader draws. The share's entityGroups
and operations columns are typed from the wire, and
docs/development.md says where the vendored client package comes from
and how to refresh it.
vendor/developmentfinance-client-0.1.0.tgz is the TypeScript client
at the sdk commit 69f7439, with the contract timeouts, assumptions on
candidates and datasets, and the generated warning codes;
pnpm-lock.yaml carries its integrity.
…ent's wire types

SearchInput, DatasetFormat, the entity-options scope and the page-spec
subsets in definitionMembers.ts, frozenObservations.ts and
measureMenu.ts derive from the wire types. A spec whose stat vars the
site cannot read is UnreadableSpec, one side of the PartSpec union,
and statVarsUnreadable is its type guard; flattenSpec returns the
union with no cast. The stream's facet_options entries are
FacetOptionsEntry, FacetOptions without source, which the stream does
not send.

The suggestion state, the streamed observations and facets, the cards'
parts and the share, event and sentence readers carry their types, and
hasDefiniteSpec narrows a hydrated part to one with a spec. The shared
edits move into sharedEdit.ts as pure functions with tests: a definite
spec takes an edit when its slots hold the edit's axis, readable or
not, and a spec without slots is skipped. fetchDatasetCapabilities,
which had no caller, is gone.
The package resolves its key when the client is built, carries each
response's trace id, follows the download rules in a browser and
refuses a secret key in a worker. pnpm-lock.yaml pins the new
tarball's integrity.
The package keeps decimals exact, follows only a 302 from the files
route, raises a non-retryable 429 poll and checks where a browser's
followed redirect ends. pnpm-lock.yaml pins the new tarball's
integrity.
The client refuses a files-route 302 whose Location is empty.
Split mid-sentence colons and semicolons into sentences, and drop
em dashes and contrast tails in the comments the generated client types
brought, and in the vendoring note in docs/development.md.
The package carries the tightened JSDoc in its type declarations.
@jm-rivera
jm-rivera added this pull request to stack #44 October 21, 2026 15:15
@jm-rivera
jm-rivera marked this pull request as ready for review October 23, 2026 06:25
Base automatically changed from feat/answer-assumptions to dev October 23, 2026 11:28
# Conflicts:
#	db/schema.ts
#	package.json
#	pnpm-lock.yaml
#	src/components/organisms/DataSearch/SearchSentence.tsx
#	src/lib/search/datasetEntries.ts
#	src/lib/search/departures.ts
#	src/lib/search/readings.ts
#	src/lib/search/searchClient.ts
#	src/lib/search/searchShare.ts
#	src/lib/search/searchShareRequest.ts
#	src/lib/search/sentencePlan.ts
#	src/lib/search/test/datasetEntries.test.ts
#	src/lib/search/test/departures.test.ts
#	src/lib/search/test/fixtures.test.ts
#	src/lib/search/test/parsedEdit.test.ts
#	src/lib/search/test/searchShareRequest.test.ts
#	src/lib/search/test/view.test.ts
#	src/lib/search/types.ts
#	src/lib/search/view.ts
@jm-rivera
jm-rivera merged commit 09e48b5 into dev Oct 23, 2026
1 check 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