Repository navigation
Serve the v2 parameter tree one level at a time, with nested bracket schedules and the reform view - #4046
Serve the v2 parameter tree one level at a time, with nested bracket schedules and the reform view#4046anth-volk wants to merge 23 commits into
Conversation
6414a0e to
b05676e
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>
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 |
Fixes #4045
Summary
Plan item 1.17:
/v2/parameters/childrenfor the parameter tree of the app's newer workspace (v3). It is stacked on #4042 (1.16), 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 onf5c1d9b4685b(1.15). 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; an unlabelled schedule…rates.ukis "Uk");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 1.16.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; 1.16'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: an open decision for a person. 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. So 558 UK and 54,710 US reform-view labels differ from what v3 shows today, and the order differs at some levels. The plan labels children with their stored labels, and 1.14 deliberately publishes no second copy of each stored label, so this PR keeps .py's labels. The options are:Schedule labels. Schedules get their stored labels once the Public API depends on the .py release containing Publish the model metadata the Public API can only read through a private method policyengine.py#701, which publishes each bracket schedule as a node with its label. Until then a schedule has no node row and gets a label from its name ("Uk", "Al").
Trees for other versions stored earlier: left out
/v2/parameters/childrenreturns 503 for it until a person approves that migration. 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
Testing
CI on this branch is cancelled at the user's request. The evidence is local; each commit failed first for the expected reason and then passed, unless it only adds tests.
b8c8d751eDerive each catalog version's parameter treeModuleNotFoundError873218365Store each catalog version's parameter tree in its own tableKeyError: 'parameter_tree_children', the revision chain, no child revision,NoSuchTableErroralembic checkclean; privilege apply and verifyb35c854dcWrite each stored catalog version's parameter tree during publication8bd67742fServe /v2/parameters/children from the stored parameter tree472a6286aReturn parameter tree labels in the form v3 shows thema38f6c018Write the parameter tree only for the catalog versions a publication publishesc5bb6301dPublish the parameter tree as the catalog writer role in a testInsufficientPrivilegewith the INSERT grant revoked20d9a419dDocument the children route's statuses and page the reform view in a testcc4489863Test the parameter tree with records shaped like PolicyEngine.py's output97904be68Type a parameter tree row's child type with its two valuesstralembic checkclean9983a3aedDrop the reform rule for parameters without flagsbfe79da42Declare exactly the errors the parameter tree route returns6c3bbcfe9Give the installed catalog version its parameter tree when it has none89156a2cbTest that parameter tree responses are cached like other metadata4d14c0dc3Write no parameter tree for a version stored before catalog format 203d081864Test the parameter tree's link to stored parameters by nameb7300b3c0State the parameter tree's real rule in its documents and messagesd6f4af31bName the catalog writer in the runtime-role rule and the Alembic skill37ab9482fLook up a stored catalog version's ID and format in one place2e41451eaAdd the catalog format 2 condition to the tree writer's descriptionsedb0a8ec4Test the tree's parameter IDs with stored IDs unlike the catalog's1cc6f620fCheck that a version stored before catalog format 2 returns the tree 5034519b8e96Say which metadata read the runtime-role rule's tests coverThe commits that add a role-based test (
8bd67742f,c5bb6301d) 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 the final commit:
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.Review
Two review rounds were run on this branch before it was rebased onto 1.16.
472a6286a. The same round's quality findings led toa38f6c018(no trees from earlier releases' stored rows),c5bb6301d, and20d9a419d.cc4489863,97904be68,9983a3aedandbfe79da42.After the rebase,
6c3bbcfe9gives the installed version its tree, and89156a2cbadds the caching tests.4d14c0dc3: 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 in03d081864,b7300b3c0andd6f4af31b.3885872ae), found no substantiated bugs. Its quality findings are fixed in:37ab9482f: one stored-version lookup shared with the stored-version comparison;2e41451ea: the format 2 condition in the remaining descriptions;edb0a8ec4: tests that tell the stored parameter IDs from the catalog's;1cc6f620f: a test that the format 1 case returns the 503;4519b8e96: the rule statement narrowed to what CI checks.🤖 Generated with Claude Code