Repository navigation
Serve the v2 parameter tree one level at a time, with nested bracket schedules and the reform view - #4046
Conversation
6414a0e to
b05676e
Compare
b05676e to
4519b8e
Compare
|
Review at Verdict: the tree builder and the route look correct. One design gap to settle before the label decision lands. Stacked on #4042, so I reviewed only
Checked and fine: What I ran: fresh clone, 🤖 Generated with Claude Code |
4519b8e to
bda7e11
Compare
33f77cb to
52c2989
Compare
52c2989 to
6a90c67
Compare
A version's tree comes from its stored parameters and parameter nodes alone. A "[" starts a child as a "." does, so a bracket schedule is one node whose children are its brackets, labelled "Bracket 1", "Bracket 2" and so on; a path without a stored label gets one made from the last part of its name. Each row also records whether a reform can change it, with the same rules as the tree v3 builds from v1 metadata, and its position among all siblings and among those a reform can change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The new table parameter_tree_children holds one row per node and parameter of a version, under its parent's path, with its position among all siblings and among those a reform can change. Two unique indexes, (version, parent path, position) and (version, parent path, reform position), serve one level of the tree in one read. Rows are deleted with their version or parameter. The revision is autogenerated with no corrections and writes no rows. The Public API's runtime role may read the table and the catalog writer may read and insert rows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Publication now writes, in its transaction and under its lock, the parameter tree of every stored catalog version that has parameters but no tree: the versions it publishes and versions stored before the tree existed. The rows are derived from catalog rows already stored, so writing them is a row-only, idempotent insert that changes no other table; a version that has its tree keeps it. Publication fails if a version is still without a tree, and the evidence lists each version whose tree was written. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The route now reads one level of the version's stored tree with one indexed read, so a bracket schedule is one node whose children are its brackets, and every child has its stored or generated label. reform_changeable=true lists only the children a reform can change, sorted by label, with child counts that count only those. The route reads its query through the shared strict models, so unknown or repeated parameters and an unsupported country return 422, and a version whose tree has not been written returns 503. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v3's tree shows every label with its underscores as spaces and its first letter upper-cased, but stored labels were returned unchanged while generated labels took that form, so one page could mix both and differ from v3 (for example "assumed uncapped single bus fare"). Stored labels now take the same form in both views, and the reform view sorts by it. Unlabelled schedules get a label from their name, such as "Uk" and "Al". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…publishes Writing trees for versions stored before the tree existed derives rows from what earlier releases stored, which the migration rules leave to a human migration decision, so publication now writes the tree only of the versions it publishes, and fails if one of those is still without a tree. A version without a tree returns 503 with a message that names it and says it was published before the tree existed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The catalog writer's INSERT grant on the tree table was never used as that role. The new test publishes the fixture catalog through a connection that acts as the catalog writer with only its declared privileges, and checks each published version's tree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…test The route's OpenAPI description, its changelog fragment and the Stage 9 document now say that invalid query parameters, including a malformed policyengine_version and an unsupported country, return 422 (both returned 400 before), and that a version published before the tree existed returns 503. A service test pages through the reform view in label order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tput
The new unit and Postgres route tests build the tree from bracket parameters
labelled the way PolicyEngine.py generates them ("... (tier 1 amount)"), a
breakdown parameter labelled from its parent ("Standard deduction (Single)"),
a schedule without a node row, as the locked PolicyEngine.py publishes, and
one with a labelled node row, as the release that publishes schedules will.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ParameterTreeChild.child_type is now typed Literal["node", "parameter"], the two values its check constraint allows, so reads keep the type. The column is unchanged, and alembic check finds no new operations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Trees are written only for versions published with catalog format 2, whose parameters always have both flags, so the rule that treated a parameter with neither flag as changeable could not apply. It is removed with its test and the documentation about it, and the service tests now store their fixture parameters' flags as a format 2 publication does. The children transformation's docstring now describes the displayed label form. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The route's OpenAPI 503 now says that a catalog version published before the tree existed has no tree, and that the message then names the version; the other metadata routes keep the shared description. Like them, the route declares no 400, since every invalid query is a 422. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When an earlier release stored the installed catalog version without a tree, publication now writes one in the same transaction, built from the installed packages' catalog records, the same tree a fresh publication writes. Only the stored parameters' IDs are read, by name, so the rows reference them; the stored rows were already checked against the catalog. Other stored versions still get none, and publication fails if a version in the catalog is left without a tree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A version's tree never changes, so the route serves an ETag, a day-long Cache-Control for a named version and five minutes for the running one, a Vary on Origin, and a 304 for a matching If-None-Match, as the other metadata routes do; a version without a tree gets a 503 with none of them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Such a version's parameter rows store no economy or household flags, so a tree built from the installed catalog would carry flags, reform positions and reform counts its own rows do not have. Publication now writes a tree for a stored version only when its catalog format is 2, and the check that every catalog version has a tree leaves such a version out; it keeps returning the 503 that names it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A unit test builds a catalog version's tree with stored IDs for every parameter and with none, which fails naming the missing parameter. The Postgres tests share one tree query, and the fail-closed test names the argument it now receives (catalog countries). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The module and model docstrings, the runbook, the changelog fragments, the unavailable-tree error and service docstrings, the 503 message and the route's OpenAPI descriptions now say that publication builds trees from the installed packages' catalog, for the versions it publishes and for the installed version stored with catalog format 2 but without one, and that any other version has none and returns the 503. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rule api.v2.runtime-privileges-tested-as-runtime-role's statement and the Alembic skill's privileges section now say the integration tests also run the metadata services as the runtime role and catalog publication as policyengine_v2_catalog_writer, and the skill's command runs the two newly scoped modules. The generated rules document is re-rendered. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
stored_version returns a country's stored version with its ID and catalog format (None before format 2). The stored-version comparison and the parameter tree writer both use it instead of their own copies of the same query and rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The publish_catalog docstring and the tree writer's module docstring now say the installed version gets a tree only when it is stored with catalog format 2, and the evidence field's comment says it is also empty when the installed version was stored before that format. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The catalog's parameter IDs and the stored ones were both derived from the version and name, so the tests could not tell which the tree used. The unit test now gives each stored parameter a new ID, and the Postgres test first stores the version with new parameter IDs, so a tree built with the catalog's IDs fails both (a mismatch, and a foreign-key error). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Postgres route test stores the fixture catalog, makes it look published before catalog format 2, removes its tree and republishes it: publication writes no tree, and the route returns 503 with the message naming the version and no cache headers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Of the metadata reads, only the parameter tree read runs as policyengine_v2_runtime in the integration tests, so the rule's statement and the Alembic skill now name that read instead of the metadata services. The generated rules document is re-rendered. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…imit The parameter tree's tests took test_metadata_routes.py to 1,657 lines and test_metadata_service.py to 1,623, over the 1,500-line limit for test code. Each test moves unchanged, apart from its helpers' names: - test_metadata_route_caching.py: entity tags, cache lifetimes, kept whole-version responses and Vary; - test_metadata_parameter_tree_routes.py: /v2/parameters/children; - test_metadata_service_parameter_tree.py: the stored tree, one level at a time; - test_metadata_service_catalog_records.py: whole-version variables and parameters fetched by name. The helpers several of these files use move to tests/fixtures/v2_metadata_routes.py and tests/fixtures/v2_metadata_service_helpers.py without their leading underscore (_client becomes metadata_route_client, _service catalog_service, _model_version stored_model_version). The catalog_session fixture moves to tests/fixtures/v2_metadata_service.py, which each service file loads through pytest_plugins. test_parameter_value_listing_route.py imports the route helpers from their new module. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PolicyEngine.py stores a label it makes for a parameter that has none: a
bracket field gets its schedule's label and the bracket ("Income Tax main
rates (bracket 1 rate)"), and a breakdown parameter its breakdown parent's
label and one value per level below it ("Standard deduction (Single)"). v3
shows such a parameter by the last part of its name today ("Rate",
"SINGLE"), so the parameter tree now does too.
A bracket label counts as made by PolicyEngine.py when it is exactly the
schedule's stored label with the bracket number and field. A breakdown label
counts when it is an ancestor's stored label followed by one value per level
below that ancestor, each the key itself, a label the version's enum
variables give the key, or a dimension label followed by the key; each value
is matched whole, so an enum label containing ", " is still one value.
Publication passes the enum labels from the catalog's variable records. Any
other stored label is kept, and other catalog reads, such as the parameter
routes and the search file, keep PolicyEngine.py's labels.
Over the policyengine-us 2.2.1 and policyengine-uk 2.132.6 trees, this
recognises every label PolicyEngine.py generated (58,464 US, 533 UK) and no
label the packages write, apart from the 10 UK LHA labels such as "Maximum
LHA (category A)", whose form is the one PolicyEngine.py gives US labels such
as "Fee level A".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6a90c67 to
689c3bc
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #4046 +/- ##
==========================================
- Coverage 88.22% 88.21% -0.01%
==========================================
Files 285 288 +3
Lines 16843 17129 +286
Branches 2741 2814 +73
==========================================
+ Hits 14859 15111 +252
- Misses 1302 1315 +13
- Partials 682 703 +21 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Master added revision e86f19a6c4da, which drops the UK area impact tables' map coordinates, on the catalog format 2 revision f5c1d9b4685b. The parameter tree revision 3869eb814a90 was chained on the same parent, so it now revises e86f19a6c4da and stays the single v2 head: - migration_target.py names 3869eb814a90 as the head; - the revision chain test lists e86f19a6c4da between the two, and the tree revision's own test finds it as the child of e86f19a6c4da; - the coordinate removal's unit test no longer asserts that its revision is the head, and its lifecycle test compares that revision, not head, with its parent, because head now also creates the tree table; - the tree table's lifecycle test downgrades to e86f19a6c4da. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With master's area impact coordinate tests, test_alembic_v2_lifecycle.py reached 1,569 lines, over the 1,500-line limit for test code. The lifecycle tests of the catalog tables' revisions (catalog format 2's columns and the parameter tree) move unchanged to test_alembic_v2_catalog_tables.py, with their revisions and helpers. The three helpers both modules use move to tests/fixtures/integration/alembic_v2.py without their leading underscore: _disposable_url becomes disposable_v2_url, _config alembic_v2_config and _columns table_columns. The v2 Alembic check's script, its workflow test and the testing guide's commands name the new module. It reads the same disposable database settings as the lifecycle tests, which the job already sets. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…removal The merge of master re-pointed revision 3869eb814a90 onto e86f19a6c4da by editing its parent. As e86f19a6c4da was itself regenerated on each revision that reached master first, 3869eb814a90 is now generated again, with --rev-id 3869eb814a90, on a database at e86f19a6c4da, and formatted as the other revisions are. The generated operations are unchanged; only the creation date differs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes #4045
Summary
/v2/parameters/childrenfor the parameter tree of the app's newer workspace (v3). It is stacked on #4042, and v3 does not use it until it moves to the v2 routes.Tree table. The new
parameter_tree_childrentable holds one row per node and parameter of a catalog version, under its parent's path (""for the top level). Each row has:Two unique indexes, (version, parent path, position) and (version, parent path, reform position), serve one level in one read.
Revision
3869eb814a90is chained one86f19a6c4da,master's revision that drops the UK area impact tables' map coordinates (Drop the x and y columns from API v2's UK area impact tables #4103), which itself follows Store the metadata v3 reads in the v2 catalog from PolicyEngine.py's public records (catalog format 2) #4023'sf5c1d9b4685b. It is autogenerated with no corrections and creates the table, its two listing indexes and an index onparameter_id. It writes no rows. Rows are deleted with their version or their parameter.Writing at publication.
parameter_trees_written(model, version, rows).Tree rules.
[starts a child as a.does, so a bracket schedule is one node whose children are its brackets, labelled "Bracket 1", "Bracket 2" and so on, in bracket order. A bracket's own children (rate,threshold) are unchanged.gov.whose economy or household flag is set and whose path names neithertaxsim,gov.abolitionsnorpycache, and every node above such a parameter. These are v3's rules (app/src/libs/buildParameterTree.ts).The route reads one level from the table, with the leaf summary the parameter routes return (including
economyandhousehold).reform_changeable=truelists only what a reform can change, sorted by label, with child counts that count only those.Cache-Control(a day for a named version, five minutes for the running one),Varyand a 304. A version's tree never changes. The 503 is not cached.Route contract changes
/v2/parameters/childrenhas no callers in this repository outside tests, and none in the app'smain.property; "Rate" instead of "Income Tax main rates (bracket 1 rate)");reform_changeable. The route's query goes through the shared strict models, so an unknown or repeated parameter, an unsupported country and a malformedpolicyengine_versionreturn 422. An unsupported country and a malformed version returned 400 before Serve v2 metadata in parts loaded on demand, cacheable per catalog version #4042.Measured
Locally, against the installed catalog with format 2 fields (the .py branch containing PolicyEngine/policyengine.py#701, 6.10.0), service level, 20 requests each:
gov,gov.irs, a 42-bracket schedule, the reform view ofgov)EXPLAIN ANALYZEof a reform-view levelix_parameter_tree_children_reform_listing, 0.07 msStaging times need a deploy; #4042's release check records route times on each candidate.
Declared differences from v3's current tree
A node with nothing a reform can change below it is left out of the reform view. v3 shows such nodes as empty groups, because v1's node entries carry their own flags.
Brackets are ordered by number. v3 sorts them as text, so "Bracket 10" comes before "Bracket 2".
Sorting by label approximates JavaScript's
localeComparewith a case-folded comparison.Labels: generated labels are shortened in the tree only (decided). The catalog stores .py's labels, and .py generates labels for parameters without a stored one (
utils/parameter_labels.py, since December 2025):v1's
/{country}/metadatainstead uses the last part of the name with underscores as spaces, and "bracket N" for brackets, which is what v3 shows today. Of the three options (accept .py's labels; shorten generated labels in the tree only; have .py also publish the raw stored label, which would change Publish the model metadata the Public API can only read through a private method policyengine.py#701 and Store the metadata v3 reads in the v2 catalog from PolicyEngine.py's public records (catalog format 2) #4023), the second was chosen.689c3bc8cimplements it:Schedule labels. Since Store the metadata v3 reads in the v2 catalog from PolicyEngine.py's public records (catalog format 2) #4023 the Public API depends on .py 6.12.0, which contains Publish the model metadata the Public API can only read through a private method policyengine.py#701 and publishes each bracket schedule as a node with its stored label. Schedules therefore show their stored labels, or a label from their name when the package gives them none. Before 6.12.0 a schedule had no node row and always got a label from its name ("Uk", "Al").
Trees for other versions stored earlier: left out
/v2/parameters/childrenreturns 503 for it. So does an installed version stored before catalog format 2, for example if a .py release carrying the flags were published frommasterbefore this stack deploys.Migration notes
metadataroute group; the route is served by FastAPI and reads Supabase (no change).alembic upgrade head, then catalog publication, which writes the trees, before the new release takes traffic.# noqais added.rules/registry.tomladdstests/integration/test_v2_metadata_routes.pyandtests/integration/test_v2_catalog_publication.pyto the scope ofapi.v2.runtime-privileges-tested-as-runtime-role, because they now run a service as a managed role: the route as the runtime role, publication as the catalog writer. The rule's statement and the Alembic skill's privileges section now say the tests run the v2 user services and the parameter tree read as the runtime role and catalog publication as the catalog writer, and the skill's command runs the two modules. A review fromPolicyEngine/core-developersis requested.Waits for
master). The branch includes a merge ofmaster(below).The decisions on the labels and on trees for other stored versions are made (above).
Testing
The evidence below is local; each commit failed first for the expected reason and then passed, unless it only adds or moves tests. The last commit's checks were run with the whole branch ("At the head" below).
b7b9f393fDerive each catalog version's parameter treeModuleNotFoundErrorfb6c23230Store each catalog version's parameter tree in its own tableKeyError: 'parameter_tree_children', the revision chain, no child revision,NoSuchTableErroralembic checkclean; privilege apply and verifyb46c44aa4Write each stored catalog version's parameter tree during publication376bb216fServe /v2/parameters/children from the stored parameter tree98b9c0b3dReturn parameter tree labels in the form v3 shows thembfed72372Write the parameter tree only for the catalog versions a publication publishes63e9d1936Publish the parameter tree as the catalog writer role in a testInsufficientPrivilegewith the INSERT grant revoked093b4e539Document the children route's statuses and page the reform view in a testc6ddfa5fdTest the parameter tree with records shaped like PolicyEngine.py's outputede52b1c3Type a parameter tree row's child type with its two valuesstralembic checkclean1e31c9ae5Drop the reform rule for parameters without flags0589038d4Declare exactly the errors the parameter tree route returnsd4515053dGive the installed catalog version its parameter tree when it has none1826bcfbfTest that parameter tree responses are cached like other metadata55c3f6c5cWrite no parameter tree for a version stored before catalog format 26277907a4Test the parameter tree's link to stored parameters by name1d3464c81State the parameter tree's real rule in its documents and messages4af6107b4Name the catalog writer in the runtime-role rule and the Alembic skillc4187adebLook up a stored catalog version's ID and format in one place7e97c0a1fAdd the catalog format 2 condition to the tree writer's descriptionsca19fb6fdTest the tree's parameter IDs with stored IDs unlike the catalog's2475b29b9Check that a version stored before catalog format 2 returns the tree 50354ab346e1Say which metadata read the runtime-role rule's tests coverd4191a253Split the v2 metadata route and service tests under the file-length limit689c3bc8cShow PolicyEngine.py's generated parameter labels in v3's short formThe commits that add a role-based test (
376bb216f,63e9d1936) also add its module to the rule's scope, sotest_every_managed_role_integration_module_runs_in_ci_and_is_in_its_rulepasses at each commit.On
54ab346e1, before the lock moved to .py 6.12.0:make testset (without coverage), under the locked .py 6.10.0: 3,657 passed.alembic check: no new operations..coveragefile or names a private repository.At the head, under the locked .py 6.12.0, checked before GitHub's rebases onto
master(as52c2989ea;git range-diffshows all 25 commits unchanged since):3869eb814a90: 11 passed.Merge of
mastera226f4966mergesmasterata69c6e2cd.masterhad added revisione86f19a6c4daonf5c1d9b4685b, the same parent as3869eb814a90, so the merge re-points3869eb814a90onto it, keeping one v2 head:migration_target.pynames3869eb814a90as the head;e86f19a6c4dabetween the two, and the tree revision's test finds the tree revision as the child ofe86f19a6c4da;e86f19a6c4da.3e09a92d7regenerates3869eb814a90with--rev-id 3869eb814a90on a database ate86f19a6c4da, asmasterregeneratede86f19a6c4daon each revision that merged before it. The generated operations are the same as before; only the creation date changed.ad826d366splitstest_alembic_v2_lifecycle.py, which the merge took to 1,569 lines (over the 1,500-line limit for test code). The catalog format 2 column and parameter tree lifecycle tests move unchanged totest_alembic_v2_catalog_tables.py, and three shared helpers move totests/fixtures/integration/alembic_v2.pywithout their leading underscore. The v2 Alembic check's script, its workflow test and the testing guide name the new module. Serve a compact parameter search file for each catalog version at /v2/parameters/search-file #4049 makes the same split with the same names.alembic headsshows3869eb814a90alone;alembic checkfinds no new operations; ruff, mypy (187 files), the quality guards anduv lock --checkpass. One review round ran on the merge and the split (below).Review
Two review rounds were run on this branch before it was rebased onto #4042.
98b9c0b3d. The same round's quality findings led tobfed72372(no trees from earlier releases' stored rows),63e9d1936, and093b4e539.689c3bc8c. Its quality findings are fixed inc6ddfa5fd,ede52b1c3,1e31c9ae5and0589038d4.After the rebase,
d4515053dgives the installed version its tree, and1826bcfbfadds the caching tests.Round 3, on those new commits, found one major bug. A version stored before catalog format 2 would get a tree carrying the catalog's flags, which its own parameter rows do not store. This is fixed in
55c3f6c5c: no tree for such a version, and the check that every version has a tree leaves it out. The same round's quality findings are fixed in6277907a4,1d3464c81and4af6107b4.Final pass, after the rebase onto Serve v2 metadata in parts loaded on demand, cacheable per catalog version #4042's final commits (
3885872ae), found no substantiated bugs. Its quality findings are fixed in:c4187adeb: one stored-version lookup shared with the stored-version comparison;7e97c0a1f: the format 2 condition in the remaining descriptions;ca19fb6fd: tests that tell the stored parameter IDs from the catalog's;2475b29b9: a test that the format 1 case returns the 503;54ab346e1: the rule statement narrowed to what CI checks.Merge of
master: one review round ona226f4966andad826d366found no substantiated bugs. Of its quality findings, the hand-edited parent is replaced by the regenerated revision (3e09a92d7). The new module's tests start by downgrading to a named revision, so they need the lifecycle module to run first, as the v2 Alembic check's script and the testing guide run them (Serve a compact parameter search file for each catalog version at /v2/parameters/search-file #4049's split has the same order). The moved URL helper repeats the check of thedisposable_v2_database_urlfixture intests/integration/conftest.py; merging the two is left for a follow-up.🤖 Generated with Claude Code