Skip to content

v3: load metadata on demand from the Public API's v2 routes, unused until the switch - #1444

Draft
anth-volk wants to merge 28 commits into
mainfrom
v2plan/1f-119-v3-parameters-on-demand
Draft

anth-volk wants to merge 28 commits into
mainfrom
v2plan/1f-119-v3-parameters-on-demand

Conversation

@anth-volk

Copy link
Copy Markdown
Collaborator

Fixes #1443

What this adds

v3 (Build, Reforms, the report pages and Ask) can load its model metadata from the Public API's v2 routes a piece at a time, and save reforms as /v2/policies policies. All of it is merged unused: it runs only once the switch below is turned on.

  • A generated client for the Public API's v2 routes.
    • app/src/api/publicApi/schema.d.ts is generated with openapi-typescript 7.13.0 from a checked-in copy of the Public API's OpenAPI document (app/src/api/publicApi/openapi.json).
    • A small openapi-fetch 0.17.0 client sends the existing v2 credentials and unwraps the success envelope.
    • bun run public-api:generate writes the types, bun run public-api:refresh <source> copies the document from an API origin, a URL or a file and regenerates, and bun run public-api:check fails when the types do not match the document.
    • The new PR job "Public API client is current" runs that check. The app has no required checks, so the job is advisory.
  • Model metadata at start: only GET /v2/model-catalog (entities, basic inputs, regions, years, datasets and the catalog version), converted to the shape v1's metadata has, so everything that reads the metadata store keeps working. Pages that only redirect into v3 (the country index, the old Home, Library and tracker addresses, and the paused Ask page) load nothing.
  • Parameters by name: a view's parameters load from GET /v2/model-catalog/parameters?name=… (up to 100 names a request) into the metadata store when the view names them. They are requested again after the model catalog reloads.
  • The parameter tree: one level at a time from GET /v2/parameters/children (reform_changeable=true). Each folder's count is the number of parameters a draft can add at or below it, taken from the search file, as v1 counts the whole tree; a folder with none is hidden.
  • Search: GET /v2/parameters/search-file, turned into the same search index v1 builds, with editability on today's date from editable and editable_changes.
  • Loading behaviour: each view waits for the parameters it needs, and a failed load says so with a retry. This covers:
    • Build's draft, its Save buttons and its comparison;
    • the Reforms page's bill and saved-reform details;
    • the report pages, which load a report's parameters before parsing its policies;
    • the bill report, the policy tree and Ask.
  • Saved reforms on /v2/policies:
    • A save creates (or, as the Public API deduplicates, finds) a /v2/policies policy, and the /v2/user-policies row points at it with policy_id.
    • A parameter edit makes a new policy and repoints the row with PATCH.
    • A row that holds only a v1 policy (legacy_policy_id) still opens.
    • A reform keeps its v1 policy ID (policyId) and its v2 policy ID (v2PolicyId) in separate fields.
    • Both reform stores build their bodies from the generated UserPolicyCreateRequest and UserPolicyPatchRequest types.
    • Parameter values from the Public API are checked where they arrive, and values sent to /v2/policies are checked as finite JSON before the request.

Merged unused: the switch

Every v2 path sits behind V3_API_ROUTES in app/src/libs/flagship/v3ApiRoutes.ts. It is a constant, not a runtime flag or an environment variable, and it stays 'v1' here. Hooks and components choose their v1 or v2 implementation from it when the module loads. Tests that cover the v2 paths mock it to 'v2'.

With 'v1', the app keeps today's behaviour:

  • The same requests: GET /{country}/metadata for the whole model, and v1 policies for saved reforms.
  • The same waiting: the views that did not wait for the whole metadata before still do not.
  • The same failure displays: the new failure messages show only on v2. The v1 paths of the report and household policy hooks are today's code, and v1 tests cover each failure display.

Turning the switch on is a later pull request.

Environment variables

None new.

  • The client sends requests to the existing BASE_URL in app/src/constants.ts. It is built from VITE_API_URL, or from NEXT_PUBLIC_API_URL mapped onto it in calculator-app, and falls back to https://api.policyengine.org. This PR does not change where those variables come from, and I did not verify their providers here.
  • The new CI job reads no environment variables or secrets. It checks out the repository, sets up Bun as the other jobs do, runs bun install --frozen-lockfile and runs bun run public-api:check in app/.

Declared deviations

  • Ask searches on the main thread, not in v3's search worker. The worker answers only its widening search, so Ask would need a new request type there and a one-off worker client of its own. The Ask route currently redirects to Build, so no reader runs this search; it can move when Ask returns.
  • v2 search results show no current values. The search file carries none, and loading them for each search while typing would mean a request per search.
  • Build sends two parameter requests for a draft: its own parameters (with the growth indexes) at once, then the members of their groups once the search file names them.
  • The report pages keep their own placeholders rather than a shared loading page.
  • Variables come in the next pull request.
  • Parameter labels: the app shows the labels the Public API stores, falling back to v1's ("bracket N", or the last part of the path) when none is stored. Whether .py should supply these labels is an open question.

The generated document

The OpenAPI document keeps the Public API's internal migration stage numbers (Stage 10, 11 and 12A) in some route descriptions. They are kept as generated.

What it waits for

Testing

CI runs on this branch are cancelled at the author's request; the evidence is local.

  • Every commit: each test was written first and seen failing before the code, and bun run check (tsc, ESLint, Prettier), bun run stylelint and the full app suite passed before the commit. The calculator-app's type check and lint ran where it changed.
  • The final commit: make typecheck and make lint (app and calculator-app) pass, the app suite passes with 6,059 tests (10 skipped) on Node 26 as CI uses, the website tests pass with 483, and bun run public-api:check reports the types match the document.
  • The lockfile is written by the pinned bun@1.2.21. It changes only to add openapi-fetch and openapi-typescript and the packages they depend on.

Review

Three review rounds, each fix test-first:

  • Round 1 (two major bugs, three minor):
    • Major: parameters loaded on demand were not requested again after the model catalog reloaded.
    • Major: the report pages parsed a fresh report's policies before their parameters loaded.
    • Minor: pages that only redirect started v1's metadata download.
    • Minor: a deep folder opened from search showed the top-level grid while it loaded.
    • Minor: the Reforms bill detail and the Ask page did not wait for, or report the failure of, their parameters.
  • Round 2 (four minor bugs):
    • A failed parameter load left the Report page loading for good.
    • With the switch off, the report hook waited for v1's whole metadata.
    • Three more redirect-only pages still loaded metadata.
    • The tree's rows showed no current-law value while it loaded or after it failed.
  • Round 3 (three minor bugs):
    • The tree's folder counts came from the Public API's direct-child counts.
    • Some failure displays differed from today's with the switch off.
    • The chat's draft step could reach the newest answer instead of its own.

This replaces #1346 and #1347, which were closed on 2026-10-20 without merging.

🤖 Generated with Claude Code

anth-volk and others added 28 commits October 26, 2026 18:55
The app gains types for the Public API's v2 routes, generated with
openapi-typescript from a checked-in copy of its OpenAPI document
(app/src/api/publicApi/openapi.json), and a small openapi-fetch client
that sends the existing v2 credentials and unwraps the success envelope.

- `bun run public-api:generate` writes schema.d.ts from openapi.json.
- `bun run public-api:refresh <source>` copies the document from an API
  origin, a URL or a file, then regenerates.
- `bun run public-api:check` fails when schema.d.ts does not match the
  document; the new PR job "Public API client is current" runs it. The
  app has no required checks, so it stays advisory.

Nothing calls the client yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…itch

v3 gains one place that chooses its Public API routes, the constant
`V3_API_ROUTES` in `libs/flagship/v3ApiRoutes.ts`. It stays 'v1' until
the switch, so nothing users reach changes: the workspace and the older
report builder still load the whole model from `GET /{country}/metadata`.

With the constant at 'v2', a v3 workspace page loads only
`GET /v2/model-catalog` at start: the catalog version's PolicyEngine.py
version, entities, basic inputs, regions, years and datasets, in the
shape the metadata store already holds. Parameters and variables start
empty, to be loaded on demand, and no whole parameter tree is built.
The workspace counts as ready only with metadata from the routes it uses,
and a retry asks those routes again. The older report builder stays on
v1 either way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…routes

On the v2 routes v3 does not download the whole metadata, so its search,
add bar and draft editor need another source for which parameters a
reform can take. That source is the catalog version's search file
(`GET /v2/parameters/search-file`), named by version so the browser can
keep it.

- `fetchParameterSearchFile` reads the file and refuses a format this app
  does not read.
- `buildV2SearchCatalog` turns it into the collection v3's existing search
  builders take (unchanged): v1's fallback labels for unlabelled
  parameters and nodes ("bracket N" for a bracket), the parameters a
  reform cannot change kept for their labels only, and no values (they
  load on demand). Which parameters a draft can take comes from each
  entry's editability for today's date. It is built once per file and date.
- `useV3SearchCatalog` gives search entries, the index, the addable paths,
  concept clusters, labels and a status: from the whole metadata on the
  v1 routes, from the search file on the v2 routes, where it stays
  'loading' until the file arrives and 'error' with a retry if it fails.
  `useV3SearchFileInBackground` starts the download once a page renders.

Nothing reads it yet; the constant `V3_API_ROUTES` is still 'v1'.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On the v2 routes the metadata store starts with no parameters. Views now
have a way to name the parameters they show and have them load:

- `fetchCatalogParameters` asks `GET /v2/model-catalog/parameters` for up
  to 100 names a request, at the catalog version, and returns each
  parameter in v1's shape (values dated as v1 dated them, v1's fallback
  label where none is stored), the labels of every node above them for
  breadcrumbs, each parameter's catalog ID and the names the version
  does not have.
- The store's `catalogParametersLoaded` merges them, for the current
  country and catalog version only.
- `useEnsureV3Parameters(names)` loads what the store lacks in one request
  and reports loading, error (with a retry) or ready; names already
  loaded, or that the version does not have, are not asked for again.
  `useLoadV3Parameters` returns a function that resolves once names are
  loaded, for actions such as adding a parameter.

On the v1 routes, where the whole metadata holds every parameter, both
report ready with the metadata and send nothing. `V3_API_ROUTES` is still
'v1', so users reach none of this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On the v2 routes the metadata store holds only the parameters loaded on
demand, so each v3 view that reads current law or parameter labels now
names what it shows and waits for it, with the loading or error state it
already has. On the v1 routes the same hooks report the metadata's own
status, so what users see is unchanged.

- Build: waits for the search file and for the draft's parameters (its
  provisions, their groups' members once the search file names them, and
  the indexes a value can grow with); a picked parameter's values load
  before it joins the draft, and a failure says so. The add bar, the
  draft table and the provision details read what a draft can take from
  `useV3SearchCatalog`, and the comparison picker waits for the compared
  reform's parameters.
- Ask: matches from the search file, says it is loading until the file is
  in, loads a match's current law before adding it, and loads a chat
  reform's parameters before building its draft.
- Your reforms, a report and a bill's report load the parameters of the
  reform, policy or bill they show in one request, and retry them.
- The v3 page layout starts the search file's download behind the page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…log loads

On the v2 routes a v3 page (Ask, Build, Your reforms) now waits for the
small `/v2/model-catalog` behind one short loading page, and says "Could
not load the model" with a retry if it fails. Everything else the page
shows waits in its own skeletons, with no time limit, as before. On the
v1 routes the page shows at once, as today.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ildren

On the v2 routes there is no whole parameter tree in the store, so the
add bar's policy browser now reads the tree one level at a time from
`GET /v2/parameters/children`, listing only what a reform can change: the
top level when it opens, then each folder as it is opened, one request a
level (every page of it), each kept for the session. The trail's labels
come from the levels above the open folder; a folder opened from search
loads them. A folder's parameters load their current law in one request
so the rows show it. Placeholders stand for a level until it arrives,
with no time limit, and a failure offers a retry.

The browser's markup moved into `PolicyBrowserView`, shared by the
whole-tree browser (the v1 routes, unchanged) and the on-demand one;
`V3PolicyBrowser` picks between them by `V3_API_ROUTES`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With `V3_API_ROUTES` at 'v2', a reform saved to the account becomes a
`/v2/policies` policy (`POST /v2/policies`, for the loaded catalog
version and its tax-benefit model) and its `/v2/user-policies` row points
at it with `policy_id`. Each dated value names its parameter's catalog
ID, which comes with the parameters loaded on demand; any not loaded yet
load first. Editing a reform's parameters makes a new policy and repoints
the same row with `PATCH /v2/user-policies/{id}` (`policy_id`), so its ID,
name and date stay; a rename only patches the name. Rows saved before the
switch that hold only a v1 policy still open, read from
`GET /{country}/policy/{id}`. Each v2 policy is fetched once per session.

On the v1 routes, and for reforms kept in the browser, nothing changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Parameters loaded on demand live in the metadata store, which every
metadata load empties, but the queries that loaded them stayed cached,
so after another page reloaded the metadata (for example the older
report builder, then Build again through browser history) a draft or a
reform kept its placeholder and a save failed for lack of catalog IDs.

Each load of the v2 model catalog now has an ID in the store
(`v2LoadId`, the load's request ID). Batch queries are keyed by it, and
`catalogParametersLoaded` takes parameters only for the current load, so
after a reload the same names are asked for again and arrive in the new
store. The v2 catalog version and load have one selector each.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The report hooks parsed each policy against the metadata store as soon
as the metadata was ready. On the v2 routes that is the small model
catalog, with no parameters loaded yet, so parsing failed ("Missing
parameter metadata") and the report page, which takes the names to load
from the parsed policies, never asked for them.

`useV3Policies` now reads each policy as stored (`GET /{country}/policy/
{id}`, under its own query key), makes sure of the parameters the stored
policies name (loaded in one request on v2; already in on v1), and only
then parses them; a policy that cannot be read or parsed is listed as
failed, and retry asks again for what failed. `useFlagshipReport` and
`useFlagshipHouseholdPolicies` both use it, so they share the stored
policies as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The country index and the tracker's old address only send the browser on
(into v3 when it is on), yet they started v1's whole-model download first.
`MetadataBootstrap` now picks a page's routes from all its segments with
`metadataRoutesForPage`: once `V3_API_ROUTES` is 'v2' those two pages load
nothing, and the page they redirect to loads its own. Before the switch
they load v1 as today. The country-ID architecture document describes
the routes argument.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…loads

The on-demand policy browser treated a level that was still loading like
a path the tree does not hold, so a deep folder opened from search showed
the top-level grid until every level of its trail had arrived. Now any
level of the open folder's trail that is still loading keeps the
placeholders up, and only a path that is missing once every level is in
falls back to the top level.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…when they fail

On the v2 routes a bill's or a saved reform's parameters load when its
detail opens. The bill detail showed its provisions and enabled "Run
impact report" and "Open as draft reform" before they arrived, and the
saved-reform detail kept its placeholders forever when the load failed.
Now, as on the bill's report page, both details show placeholders and
keep their actions disabled while the parameters load, and a failed load
shows "Could not load the parameters" with a retry. On the v1 routes
neither detail waits, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On the v2 routes a listed match's current law loads after the match
shows, and the chat's validated reform loads its parameters before it can
join the draft. The page discarded the match rows' load status, so a row
read "current: —" while loading and after a failure; the chat built its
draft step from missing values when the load failed; and a failed load
for a picked match was an unhandled rejection.

Now a match row reads "current: loading…" until its value arrives and
"current: could not load" after a failure, with an alert and a retry. A
picked match whose parameter fails to load stays out of the draft and
the page says so, as Build does. The chat's draft step waits for the
reform's current law, and a failed load says so with a retry instead of
offering provisions without it. The search file's failure message is an
alert whose "Try again" is the design system's button.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…talog's conversion without casts

Six v2 requests cast the app's country ID to the countries the Public
API serves. They now take it from publicApiCountryId, whose type comes
from the generated client and whose list of countries that type checks;
a country the v2 routes do not serve throws before any request. The
model catalog's conversion keeps only the region types v1 listed through
a type guard, so its entries need no cast, and types its entities as the
catalog's. The report pages' test reform policy moves to the report
policy fixtures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… switch

Besides the country index and the old bill tracker, three more pages
only send the browser on into v3: the old Home launcher (to Build), the
old Library (to Reforms) and the paused Ask page (to Build). After the
switch they now load nothing, like the other two, and the page each one
lands on loads its own metadata. Before the switch they load as they do
today. A page that redirects into the older report builder keeps v1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… report as it was

On the v2 routes, when the parameters a report's policies name failed to
load (or the model catalog had failed), no policy was parsed, none was
listed as failed and nothing was loading, so the Report page said
"Loading report…" for good and the household breakdowns kept their
spinner. Such a policy is now listed as failed: the Report page shows
"Could not load the reform policy" with a retry, the household
breakdowns show their error, and retrying asks for the parameters again.
A policy that names no parameter (current law) parses regardless. The
v2 hook memoises what it parses on the stored policies and the loaded
parameters, so the policies keep their identity across renders.

On the v1 routes the report and household hooks read and parse the
policies as they did before v3 loaded parameters on demand: once the
whole metadata is in, without waiting for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ey fail

The open folder's parameters load their current law once they are
listed, but the tree dropped that load's status, so the rows showed no
value while it loaded and after it failed. Now each row holds a
placeholder for its value while it loads, and a failed load shows
"Could not load current law for these parameters" with a retry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…asks for it

useReformStore read the parameters and the v2 catalog through refs set on
render, but the catalog IDs of the parameters from the Redux store. All
three are now read from the Redux store, through selectors for the v2
parts (selectV2Catalog, selectV2ParameterIds), when the reform store asks
for them. Metadata that arrived since the last render is used, and its
arrival still does not rebuild the reform store.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… from its parameters

Reform.policyId held a v1 numeric policy ID or, on the v2 routes, a
/v2/policies UUID. The report rerun read it as a number, so a report
compared with a reform saved as a v2 policy could not be run again when
its baseline simulation could not be read. policyId now holds only the v1
ID, and v2PolicyId holds the UUID. A rerun compares with a reform that
has only a v2 policy through its parameters, from which the run makes a
v1 policy, as Build's runs do.

The v2 reform store's create and PATCH bodies now use the generated
UserPolicyCreateRequest and UserPolicyPatchRequest types, so they are
checked against the OpenAPI document.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On the v2 routes the draft's current-law values load after the page
opens, and the table shows placeholders until they arrive. "Save" and
"Save and run" stayed enabled meanwhile, though saving keeps only the
values that differ from current law. Both now stay disabled while those
values load, or after the load fails. On the v1 routes they behave as
before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… say so

On the v2 routes a saved reform chosen as the comparison loads current
law for its parameters, and "Save and run" waits while it loads. When
that load failed, the run was offered again with no message, and what
the comparison changes was read without current law. The run bar now
shows "Could not load current law for the comparison" with a retry, and
"Save and run" stays disabled until it loads. On the v1 routes nothing
changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The OpenAPI document is now the Public API's final v2 metadata routes,
exported from the stack ending at the parameter search file. What
changed in the generated types:

- The model catalog's parameters and parameter summaries always carry
  `economy` and `household` (each may still be null).
- A catalog variable always carries `is_input_variable`, `hidden_input`,
  `module_name`, `adds_parameter_path`, `subtracts_parameter_path` and
  `enum_values` (each may be null), and gains `unit` and
  `definition_period`.
- A search file parameter's `editable_changes` may be left out but is
  never null.
- The metadata routes declare 422 for malformed or unknown query
  parameters in place of 400. `/v2/parameters/children` declares
  ETag, Cache-Control and Vary on 200 and a 304; the search file declares
  those headers on its 304. Several descriptions are reworded.

The app reads these fields as before (a null `economy` or `household`
still reads as true, and missing editability changes as none), so no
code changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…search file

The on-demand browser printed each folder's `child_count` from
`/v2/parameters/children`, which counts the folder's direct children a
reform can change, and hid only folders with none. v1 counts the
parameters a draft can add at or below a folder and hides folders with
none. Each folder's count now comes from the addable parameters in the
search file the app already downloads (editable on today's date), by
path prefix, so the counts match v1 and a folder with nothing to add is
hidden. The browser shows only once the search file is in, since the add
bar waits for it, so a count is never unknown.

The test fixture now gives the Public API's direct-child counts, and
holds a folder whose only parameter a draft cannot edit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nly on v2

With the switch off, some failures showed differently from today:
- The Reforms page read any failed parameter status as a failure, so on
  a v1 metadata failure the bill detail replaced its provisions with
  "Could not load the parameters" and disabled its actions, and the
  saved-reform detail disabled its report button.
- The Report page said "Could not load the reform policy" when a v1
  policy request failed.
- The household breakdowns said "Could not load policy N" instead of the
  request's own message, and no longer counted a failed refetch of a
  policy already in hand as an error.

These failure displays now appear only on the v2 routes, and the
household hook's v1 path is again the code it had before v3 loaded
parameters on demand.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ew question

The chat's validated reform loads its parameters before it can join the
draft, and the step then updated whichever turn was newest. A question
asked during that load received the earlier answer's draft step, and the
earlier turn kept saying it was loading. The step now updates the turn
its question started, as its retry already did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…API values where they arrive

Both reform stores now build their `/v2/user-policies` bodies from the
generated UserPolicyCreateRequest and UserPolicyPatchRequest types. The
hand-written bodies and their policy-reference union are gone, along
with the type test of that union; the v1 store sends the same fields as
before.

A parameter value the Public API sends is typed only as JSON. The
catalog parameters and a v2 policy's values are now checked where they
arrive, and a value that is not a parameter value fails there, naming
its parameter, instead of being cast to the app's value type. Values a
reform sends to `/v2/policies` are checked as finite JSON before the
request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bun.lock had been written by a newer Bun than the repository's pinned
bun@1.2.21, which added a `configVersion` field and dropped the optional
`@edge-runtime` entries. It is now written by bun@1.2.21 from the base
lockfile, so it changes only to add openapi-fetch and openapi-typescript
and the packages they depend on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
policyengine-calculator-next Ready Ready Preview Oct 27, 2026 2:06pm UTC
policyengine-website Ready Ready Preview Oct 27, 2026 2:06pm UTC

Request Review

@anth-volk
anth-volk added this pull request to stack #1445 October 27, 2026 14:06

This branch was successfully deployed

2 active deployments
Preview – policyengine-website — 8793eba4 Deployed Oct 27, 2026 by vercel[bot]
Preview – policyengine-calculator-next — 8793eba4 Deployed Oct 27, 2026 by vercel[bot]
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.

v3: load metadata on demand from the Public API's v2 routes, unused until the switch

1 participant