Skip to content

Serve v2 metadata in parts loaded on demand, cacheable per catalog version - #4042

Draft
anth-volk wants to merge 33 commits into
v2plan/1f-115-catalog-metadata-fieldsfrom
v2plan/1f-116-catalog-routes-on-demand
Draft

anth-volk wants to merge 33 commits into
v2plan/1f-115-catalog-metadata-fieldsfrom
v2plan/1f-116-catalog-routes-on-demand

Conversation

@anth-volk

@anth-volk anth-volk commented Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #4041

Summary

The v2 metadata routes now serve one catalog version in parts that a browser can load on demand and cache. Before, they served paged collections only, and v3 downloads the whole /{country}/metadata response at start. v3 does not call these routes until it switches to v2.

Routes and their shapes

All responses use the usual {status, message, result} envelope.

Route Query result
GET /v2/model-catalog country_id, optional policyengine_version policyengine_version, catalog_format, model, model_version (with current_law_id, metadata_time_periods), entities (key, plural, is_person), basic_inputs, regions, datasets, economy_options (region, time_period, datasets)
GET /v2/model-catalog/variables same policyengine_version, catalog_format, variables, part_parameter_values
GET /v2/model-catalog/parameters same, plus repeated name (1 to 100) policyengine_version, catalog_format, parameters, node_labels, missing_names
GET /v2/parameters adds repeated name (up to 100) items gain economy, household
GET /v2/parameter-values adds repeated parameter_name (up to 100) unchanged
  • /v2/model-catalog: names the selected version, so a client that starts without a version learns the running one and requests every other part for that exact version. A catalog without regions (a household-only country) gets empty regions, datasets and economy_options lists, not an error. A version published before catalog format 2 has a null catalog_format and empty entities and basic_inputs.
  • Variables: every variable of the version in one response, with the catalog format 2 fields (is_input_variable, hidden_input, module_name, adds_parameter_path, subtracts_parameter_path, enum_values, unit and definition_period). /v2/variables pages carry the same fields. unit (for example currency-USD) and definition_period (year, month or eternity) are there because v3's household builder and breakdown need them for currency formatting, per-month or per-year labels and household validation; they are null for versions published before catalog format 2. part_parameter_values maps each parameter path a variable's adds or subtracts names to that parameter's current-law values (start_date, end_date, value), so a client picks the list for the year it shows; a path the version lacks maps to an empty list.
  • Parameters by name: each parameter carries its summary, economy, household and its current-law values. node_labels maps each ancestor node to its stored label or null; a bracket index such as rates[0] counts as a child of rates. missing_names lists requested names the version lacks, so a reform saved against an older version still opens. Parameter-tree leaves (/v2/parameters/children) now carry the same summary, flags included.

Caching

  • ETag and 304: every successful metadata response carries a strong ETag (a hash of its exact body). A request whose If-None-Match names it (strong or weak form, alone or in a list) gets 304 with no body. Browser scripts can read the ETag (Access-Control-Expose-Headers).
  • Lifetimes: Cache-Control: public, max-age=86400 when the request names policyengine_version, and max-age=300 for the running version, which changes on deploy.
  • Current values: /v2/parameter-values?current=true takes the request time once. The read uses that UTC day, and the response may be reused only until the UTC midnight after it (max-age=0 if the headers are written after that midnight).
  • Vary: every cacheable 200 and its 304 carry Vary: Origin, Accept-Encoding, each value once. Starlette's CORS and gzip middleware append to Vary without checking for values already present, so a small UniqueVaryMiddleware now wraps the CORS middleware.
  • OpenAPI: declares the 304 and the ETag, Cache-Control and Vary headers. 400 is declared only on /v2/tax-benefit-models/by-country/{country_id}, the one route with the country in its path.
  • In-process stores (default dependencies only; tests pass their own or none):
    • MetadataResponseCache keeps the serialized /v2/model-catalog and /v2/model-catalog/variables bodies with their ETags, under the exact version they were read from. A running-version request and an explicit request for that version share one entry.
    • Its bound is eight entries. The 6.10.0 US variables body is 4,190,824 bytes uncompressed, so the store holds about 12 MB for both countries' two routes at two versions, and about 34 MB at most.
    • Requests that arrive while a read for the same key is in progress wait for it and all receive its outcome: the stored response, or their own copy of its error response. A failed read is not kept, so the first request after it reads again.
    • SelectedCatalogStore keeps each catalog version a request has selected. Failed selections are not kept.

Contract change: 422 on every metadata route

Every v2 metadata route now reads its query through the shared query models in policyengine_api.query_parameters (CatalogQuery, CatalogVersionQuery, CatalogPageQuery and resource models built on them), as docs/engineering/skills/api-routes.md requires. Unknown parameters, repeated scalar parameters, a missing or unsupported country_id, a malformed policyengine_version and out-of-range pages return 422 before any read. Before, the older routes ignored unknown parameters and returned 400 for an unsupported country or malformed version. The release live test and the Stage 9 guide now expect 422. Only an unsupported country in the path of /v2/tax-benefit-models/by-country/{country_id} still returns 400.

Measured locally (full 6.10.0 catalog, local Postgres)

Request (US unless noted) Time Uncompressed Compressed
/v2/model-catalog 10 ms; 2 ms once stored 0.258 MB 0.025 MB
/v2/model-catalog/variables 163–195 ms read; 26 ms once stored 4.19 MB 0.589 MB
/v2/model-catalog/variables, UK 27–41 ms 0.820 MB 0.157 MB
/v2/model-catalog/parameters, 20 names 5 ms 0.016 MB 0.003 MB
/v2/parameters, page of 500 7 ms 0.130 MB 0.018 MB
Revalidation (304) 1–3 ms 0 0

The US variables payload is 0.59 MB compressed, above the plan's estimate of about 0.4 MB. Adding unit and definition_period grew it from 3.91 MB to 4.19 MB uncompressed but only from 0.583 MB to 0.589 MB compressed.

Latency: what was and was not shown

  • Shown locally: with the production engine options and a warm pool, the database's statement log showed five statements per single-read request on the two routes measured slow in production on 2026-10-21 (a page of 500 parameters, 0.8–2.6 s; one parameter's values, 0.5–0.65 s). They were the pool's liveness check, BEGIN, the catalog selection, the read and ROLLBACK.
  • Now two statements: metadata reads run in autocommit sessions on the shared pool, which refuse ORM flushes and are used only by the metadata reader. Each process also keeps the catalog versions it has selected. So such a request sends the liveness check and the read. Each further read in a request adds one statement. The pool's liveness check (pool_pre_ping) is unchanged.
  • Not shown: the production cause. It is not measured from here: the database's region relative to Cloud Run, the external connection pooler and connection reuse across requests are not visible. The release live test now times both flagged routes and the new routes on every staging and production deploy, and writes the times to the job summary even when an assertion fails. Staging times are therefore recorded after deploy.

SPM availability

Not served. Nothing stores it yet; a later item records it per catalog version at publication, and /v2/model-catalog serves it then.

Migration notes

  • Route group: metadata (adds model-catalog). The contract registry gains the three new routes, and the generated migration contract documents are refreshed.
  • What remains on the Flask path: GET /{country}/metadata is unchanged.
  • Changelog fragments: .added, .changed and .fixed.
  • Inline suppressions added: one, # noqa: E501 on the reads_parameter_nodes import in services/v2/metadata/services.py. It is a dotted module path that cannot be wrapped, and it matches the existing reads_parameter_tree import in the same module.

Environment variables

None new.

  • The code reads the existing V2_RUNTIME_DATABASE_URL_SECRET_RESOURCE (GitHub Actions secret → job env: in push.yml → deploy_cloud_run_candidate.sh → Cloud Run, resolved through Google Secret Manager).
  • It also reads V2_SUPABASE_PROJECT_REF and V2_SUPABASE_ENVIRONMENT (GitHub Actions variables, same wiring).
  • The release live test reads GITHUB_STEP_SUMMARY (set by the GitHub runner in every step) and API_BASE_URL (from the candidate step's output).
  • Not verified from here that those secrets and variables are set, though the existing live tests depend on them.

What this waits for

Testing

CI on these branches is cancelled at the user's request. The evidence is local, red-green per commit: a failing run before each change, then a passing run.

Latest updates.

  • Unit and definition period: the branch was rebased onto Store the metadata v3 reads in the v2 catalog from PolicyEngine.py's public records (catalog format 2) #4023's commits that store each variable's unit and definition_period in catalog format 2. A commit now serves both fields; the review of that commit found no substantiated bugs.
  • Final pass: the branch is rebased onto Store the metadata v3 reads in the v2 catalog from PolicyEngine.py's public records (catalog format 2) #4023's final commits (7e0fb9518 and f803106ee), with no conflicts. Four commits follow:
    • The catalog format 2 fields on MetadataVariable and MetadataParameterSummary are required (nullable) in the OpenAPI schema, so a generated client does not type them as possibly missing. Before the change, the new test failed: the schema's required list lacked the format 2 fields. After it, 1,233 v2 unit tests passed.
    • Route-level tests check that /v2/model-catalog/variables (read, then served from the stored body) and /v2/variables pages carry unit and definition_period for the US, a UK variable with its own values, and a version whose catalog_format is null. These tests pin existing behavior.
    • One test helper, taking the model name, now looks up model versions.
    • The Stage 9 guide says consistently that the household builder and breakdown read the two fields.

On the final tip, with the locked .py 6.10.0 and the test schema recreated at head:

  • the full make test set without coverage: 3,619 passed;
  • Postgres tests for metadata routes, catalog publication and the v2 schema migrations: 49 passed, with the 6 live tests skipped without a deployment;
  • ruff lint and format, mypy, quality guards and rules checks: clean;
  • the contract export produced no changes;
  • no commit adds .coverage or names a private repository.

Three review rounds ran:

  • Round 1 (2 bugs):
    • Day-long public caching of current=true values.
    • The latency evidence was wrong: a commit claimed a per-request Secret Manager call, but the loader was already cached. The commit was dropped, and the round trips were measured and reduced instead.
    • Quality findings: consistent 422, cache ETag reuse and key normalization, typing, duplication, CORS ETag, and live test timings.
  • Round 2 (2 bugs):
    • The read and its lifetime used different clock readings.
    • Responses lacked Vary: Origin without an Origin header.
    • Quality findings: OpenAPI headers and the 400 description, the build lock released before the result was stored, enforcement of read-only sessions, and layout documentation.
  • Round 3 (3 minor bugs):
    • Tree leaves lacked the flags.
    • A failed whole-version read was retried serially by each waiting request.
    • A 304 lacked Accept-Encoding in Vary.
    • Quality findings: 400 declared only on the path route, a pooled-engine autocommit test plus a test of the production combination, and documentation.

🤖 Generated with Claude Code

anth-volk and others added 26 commits October 26, 2026 21:44
The metadata routers now use CacheableMetadataRoute, which gives every
successful response a strong ETag (a hash of its exact body, so any
content or format change gives a new one) and Cache-Control public: a
day when the request names its catalog version, which never changes,
and five minutes for the running version, which changes on deploy. A
matching If-None-Match (strong or weak, alone or in a list) gets 304
with no body. Failures carry neither header, and the bodies are
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GET /v2/model-catalog returns one catalog version's model and model
version, entities, basic inputs, every region and logical input
dataset, and the economy choices, in three queries and without reading
variables, parameters or their values. A catalog without regions, such
as a household-only country's, gets empty region, dataset and choice
lists rather than an error; a version published before catalog format
2 has a null catalog_format and empty entities and basic inputs. The
result names the selected PolicyEngine.py version so a client can ask
for the other parts by that exact version. The query uses the shared
CatalogQuery model, the route joins the metadata route group and the
app's metadata workflow contract, and the migration contract documents
are regenerated.

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

GET /v2/model-catalog/variables returns all of a catalog version's
variables in name order, in one response, and adds the catalog format 2
fields (input and hidden-input flags, module, adds and subtracts
parameter paths, and enum values with their labels) to every variable
response, including /v2/variables pages. part_parameter_values maps
each parameter path a variable's adds or subtracts names to that
parameter's current-law values in start-date order, read in one query
(none when no variable names one), so a client picks the list for the
year it shows; a path the version lacks maps to an empty list. Times are
served in UTC whatever the database session's time zone, so a
response's bytes and ETag do not depend on it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GET /v2/model-catalog/parameters takes up to 100 repeated name keys
(the shared CatalogParameterQuery) and returns those parameters in name
order, each with its economy and household flags and its current-law
values by date, the stored labels of the parameter nodes above them
(a bracket index such as rates[0] counting as a child of its schedule
rates), and the requested names the version lacks, in four queries on
the existing (version, name) unique indexes. A client showing or
editing a reform loads all of its parameters in this one request. The
economy and household flags also join every parameter summary, so
/v2/parameters pages and details carry them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GET /v2/parameters accepts repeated name keys and GET
/v2/parameter-values repeated parameter_name keys (at most 100 each), so
a client can page only those parameters or their values. Because their
query contracts change, both routes now read their query through the
shared ParameterCollectionQuery and ParameterValueCollectionQuery
models: unknown and repeated scalar parameters, an unsupported
country_id and a malformed policyengine_version are rejected with 422
before any read (the last two returned 400 from the read before, and
unknown parameters were ignored). The service and connector take the
search and the names as one ParameterFilter, like ParameterValueFilter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Measured against the full 6.10.0 catalog in a local database, every new
query uses an existing (version, name) or (version, code) unique index
and takes under 4 ms; the cost of /v2/model-catalog/variables (about
160 ms, 3.9 MB, 0.58 MB compressed) is reading 6,167 rows and building
their models. A published catalog version never changes, so each
process now keeps the serialized bodies of /v2/model-catalog and
/v2/model-catalog/variables once built, in a bounded store (eight
route, country and requested-version combinations, least recently used
dropped first) shared by every request of the default dependencies.
Concurrent first requests wait for one read, and failed reads are not
kept. A kept variables response takes about 25 ms, most of it
compression. Tests pass their own store or none.

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

The release workflow's live metadata test, run against each staging and
production Cloud Run candidate, now loads each country's catalog in
parts: /v2/model-catalog for the running version, its variables for
that exact version twice and once with If-None-Match (which must get
304), and one parameter by name. It appends each request's time and
body size to the GitHub job summary (GITHUB_STEP_SUMMARY, set by the
runner in every step), so every release records the routes' times
against its own catalog. The new paths join its OpenAPI check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GET /v2/parameter-values?current=true returns the values in effect on
the request's UTC day, but its response got the same lifetime as any
other: a day when it named a version. A cache that fetched it late on
31 December would have served last year's values for up to a day. The
route now marks such a request in request.state, and the caching route
caps its lifetime at the seconds left until the next UTC midnight,
whichever lifetime would otherwise apply. No other metadata route
depends on the time of the request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the production engine options and a warm pool, the database's
statement log showed five statements for each metadata request: the
pool's liveness check, BEGIN, the catalog selection, the read and
ROLLBACK. Each metadata read is one statement on rows a published
catalog version never changes, so it needs no transaction. The default
metadata reader now opens its session from a process-owned read-only
factory bound to the v2 engine in autocommit mode (sharing its pool),
so psycopg sends no BEGIN and no ROLLBACK: three statements per
request. Other v2 sessions keep the ordinary mode, which the pool
restores when a connection returns. The pool's liveness check is
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After the autocommit change, each metadata request still sent three
statements: the pool's liveness check, the catalog selection and the
read. A published catalog version never changes, so the default
metadata reader now keeps every successful selection in a process-wide
SelectedCatalogStore, keyed by country and requested version, and later
requests skip the selection query: two statements per request in the
local statement log. Unsupported countries, invalid versions and absent
catalogs are looked up, and fail, each time. Tests pass their own
store or none.

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

The response cache kept only bodies, so the caching route hashed the
3.9 MB US variables body again on every request, and a request without
a version and one naming the running version kept two copies. Each kept
response now carries its ETag, computed once when it is kept, which the
caching route reuses, and responses are kept under the exact version
they were read from: a request without a version records which version
the process runs, so it and an explicit request for that version share
one response. The route helper is generic over the result type, so
mypy checks each operation's result against its response model.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A malformed policyengine_version returned 422 from the routes that
already used the shared query models but 400 from /v2/variables,
/v2/regions, /v2/datasets, the model routes, the detail routes, the
parameter tree and /v2/economy-options. The API routes guidance makes
the shared definitions own each parameter's validation, so every
metadata route now reads its query through them (CatalogQuery,
CatalogVersionQuery for the route with the country in its path,
CatalogPageQuery and resource models for variables, regions and the
parameter tree), with the typed dependencies defined once. Unknown and
repeated scalar parameters, a missing or unsupported country_id and a
malformed version are rejected with 422 before any read on every
route. The release check and the migration document now expect 422.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The current-law value and node-label connectors returned list[Any];
they now return CurrentLawValueRow and ParameterNodeLabelRow named
tuples, so mypy checks every use of their fields. get_model_catalog
re-implemented get_economy_options's national-region and default-dataset
lookup; both now share _validated_national_dataset with a national_region
transformation, and differ only in how they load the dataset. The part
parameter paths and the found parameter names are computed once in the
service and passed to the transformations instead of being recomputed
there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The CORS middleware exposed only the request and observability IDs and
WWW-Authenticate, so a script on another origin could not read the
ETag of a v2 metadata response to revalidate it. ETag joins
Access-Control-Expose-Headers (Cache-Control is readable without it).

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

The release live test now also times a page of 500 parameters and one
parameter's values, the two routes measured slow in production on
2026-10-21, and writes every time it reached to the job summary from a
finally block, so a release whose check fails still records its
measurements. The second variables request is labelled as possibly
served by another instance, since its time need not be a kept
response's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The parameter-values route marked a current=true request as depending
on the day but passed no instant to the read: the service took the day
from its own clock during the read, and the lifetime came from another
clock reading after it. A read just before UTC midnight whose headers
were written just after got the old day's values with a lifetime of
nearly a day. The route now takes the request's instant once, passes it
to the read as the filter's now, and records it; the caching route
gives the response the seconds left until the UTC midnight after that
instant, measured when the headers are written, and zero once that
midnight has passed. No other metadata read depends on the time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The API reflects every origin in Access-Control-Allow-Origin, and the
CORS middleware passes a request without Origin straight through, so
such a response carried neither that header nor Vary: Origin while
marked public for up to a day. A shared cache storing it could serve it
to a later cross-origin request, which the browser rejects. Every
cacheable metadata response, and every 304, now carries Vary: Origin,
added to any existing Vary value such as compression's Accept-Encoding
rather than replacing it. Failed reads are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The metadata routes' OpenAPI entries described 400 as an invalid
PolicyEngine.py version, which now returns 422; 400 remains only for an
unsupported country in the path of the by-country route. They also
declared no 304 and none of the caching headers, so the app's generated
client and readers could not see them. Every metadata route now
documents ETag, Cache-Control and Vary on its 200 response and a 304
without a body. The statement counts in the changelog and the migration
document now say exactly which requests send two statements.

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

MetadataResponseCache.get_or_build removed the per-key build lock
before storing the built response, so a request arriving in that
interval found neither and started a second read. The lock is now
released only after the response is stored (or the read failed). The
concurrency tests no longer depend on timing: one blocks the build on
an event, the other starts a second request while the first stores
its result. The documentation states the bound's cost: the 6.10.0 US
variables body is 3,910,063 bytes uncompressed, so eight entries hold
about 11 MB in the expected mix and at most about 31 MB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The autocommit read sessions were described as read-only, but nothing
stopped a flush. Autocommit rules out a per-transaction read-only
setting, and a connection-level default would carry over to writers
through the shared pool, so the factory now refuses every ORM flush
with ReadOnlySessionError, and the docstring says exactly that only the
metadata reader opens these sessions and that statements passed to
execute are not checked. A test pins that the metadata reader factory
is the only user.

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

The layout guide listed neither the metadata route package's
query_dependencies and response_cache modules nor the service package's
selected_catalogs store; it now describes each and where process-wide
state belongs. FoundParameters held ORM rows and a derivable name set
in the framework-independent types module; it is removed. The service
passes the parameters, their values grouped by name (through the now
public dated_values_by_parameter_name transformation) and the missing
names to the transformation, which no longer recomputes any of them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
/v2/parameters/children built each leaf's parameter summary by hand
from a few selected columns and left economy and household out, so a
leaf always reported null, which the shared schema reserves for
versions published before catalog format 2. The tree read now selects
the stored Parameter, and each leaf is built with metadata_parameter,
the same builder the parameter routes use, so the two can no longer
differ.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MetadataResponseCache held a build lock for the whole read, and when
the read failed each waiting request took the lock in turn and read
again: ten concurrent first requests with a failing read ran ten reads
one after another. A waiting request's cleanup could also delete a
newer read's lock. Each key now has at most one read in progress;
requests arriving meanwhile wait for it and receive its outcome, the
kept response or their own copy of its error response (middleware adds
request-specific headers, so error responses are not shared). Nothing
is kept on failure, so the first request after it reads again, and a
read removes only its own entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
with_cache_headers gave a 304 only Vary: Origin, while gzip added
Accept-Encoding only to compressed 200 responses; a 304 must carry the
Vary its 200 would. Every cacheable metadata response and its 304 now
name Origin and Accept-Encoding. Starlette's CORS and gzip middleware
append to Vary without checking for values already present, which gave
"Origin" twice for cross-origin requests, so a small UniqueVaryMiddleware
now wraps the CORS middleware and lists each Vary value once. Tested
through the whole middleware stack, with and without Origin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every metadata route's OpenAPI entry listed 400 for an unsupported
country in the path, but only /v2/tax-benefit-models/by-country/
{country_id} takes the country in its path; on every other route the
shared query models reject an invalid country, version or page with 422
before any read, so 400 cannot occur there. The description now sits
only on that route.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The autocommit test used a NullPool engine, so it could not notice a
pooled connection left in autocommit mode for the next writer. It now
uses a pooled engine with one connection and checks that the writer
that takes it next is transactional (with autocommit left on, the test
fails). A new test runs the production combination of read sessions,
SelectedCatalogStore and the response cache through the routes and
counts the statements each request sends.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk and others added 7 commits October 26, 2026 21:44
The fragment did not mention Vary, the readable ETag or how requests
waiting on a whole-version read are answered, and described the
current-value lifetime loosely. It now states each as the code does.

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

Two imports in the typed-rows test named the connector functions by
long dotted paths and suppressed E501. The test now imports the two
connector modules and calls the functions through them, so it needs no
suppression.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The app's v3 household builder and breakdown need each variable's unit
(currency formatting) and definition period (per-month or per-year
labels and household validation), which catalog format 2 now stores.
Every variable response, in /v2/model-catalog/variables and in
/v2/variables pages, now carries unit and definition_period, declared
in OpenAPI and null for versions published before catalog format 2.
The 6.10.0 US variables body grows from 3,910,063 to 4,190,824 bytes
uncompressed (0.589 MB compressed, from 0.583 MB), and the documented
bound of the response store is updated to match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The format 2 fields on MetadataVariable (input and hidden-input flags,
module, the two parameter paths, enum values, unit and definition
period) and on MetadataParameterSummary (economy and household)
defaulted to None, so the OpenAPI schema left them out of required
although every response includes them, and a generated client would
type them as possibly missing. They are now required and nullable;
every constructor already passes them, and the route test fixtures now
do too. A test checks the OpenAPI required lists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
_uk_model_version duplicated _us_model_version except for the model
name. Both are replaced by _model_version(session, model_name,
version), which the tests call with the model they need.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Through the routes, with a response store: /v2/model-catalog/variables
(read, then served from the stored body) and /v2/variables pages carry
unit and definition_period for the US, for a UK variable with its own
values, and as null keys for a version whose catalog_format is null.

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

One paragraph said the household builder uses the two fields for
formatting and per-period labels, while the guide's table says the
builder and the breakdown read them. The paragraph now says the builder
and breakdown read them, for currency formatting, per-period labels and
household validation.

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

This branch was successfully deployed

No deployments
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.

Serve v2 metadata in parts a browser can load on demand and cache per catalog version

1 participant