fix(spp_studio): remove or rewrite pack items with non-evaluable CEL expressions - #444
Conversation
…expressions 24 of the 106 shipped spp.studio.pack.item records carried filter expressions referencing registrant fields or studio variables that exist in no module, so the logic they install could never translate or evaluate, and they fail spp_cel_load_testing's strict studio-validation suite (gating #432). Rewrite the 4 items where a meaningful sub-expression survives (OVC Child Age Eligibility, OVC Vulnerable Household Check, GMI Residency Requirement, Public Works Poverty Status Check); remove the other 20, which have no working near-equivalent. Every kept expression was verified through the resolver+translator path the validation test uses. Pack data is noupdate, so a migration applies the same removals and rewrites to existing databases. Bump to 19.0.2.0.2. Fixes #431
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## 19.0 #444 +/- ##
==========================================
+ Coverage 74.76% 74.81% +0.05%
==========================================
Files 509 565 +56
Lines 35008 39497 +4489
==========================================
+ Hits 26172 29551 +3379
- Misses 8836 9946 +1110
Flags with carried forward coverage won't be shown. Click here to find out more. 🚀 New features to boost your workflow:
|
…variable Cross-module sweep found that core spp_registry already defines the in_institutional_care field the item filters on - it only lacked a variable definition. Add the in_institutional_care standard variable over that field and keep the shipped expression, instead of removing the item. standard_variables.xml is not noupdate, so existing databases receive the variable through the regular upgrade data load; removals in the 19.0.2.0.2 migration drop from 20 to 19.
emjay0921
left a comment
There was a problem hiding this comment.
One change requested. The mechanics of this PR are clean and I verified them item by item — notes in a separate comment, along with a root-cause finding that is bigger than this PR.
The GMI rewrite is the only fail-open change in the set, and it reads like the others
Three of the four rewrites drop OR-branches, which makes the predicate stricter — fewer registrants qualify. That is the safe direction for a shipped eligibility template. The GMI one drops an AND-conjunct:
is_legal_resident && residency_months >= 12 → residency_months >= 12
That makes the rule more permissive: it now passes registrants the policy author wrote it to exclude. The new description — "Minimum residency duration requirement (12 months)." — drops any mention of legal residency, so nothing downstream records that the requirement was there and was removed. The PR body lists it in the same table as the three narrowing rewrites, and the note that "descriptions updated where the semantics narrowed" does not cover this case.
I checked whether the conjunct could be rescued the way Institutional Residence was: it cannot. There is no is_legal_resident field on any model (ir_model_fields on a dev DB, and a repo-wide field index), so the two consistent options are the same ones applied to the other 19 items:
- Remove the item, like the 19 whose load-bearing terms have nothing behind them. This is the option I would take — a template that silently grants eligibility is worse than no template.
- Keep item and expression as shipped, accepting that it does not evaluate until the field exists, and track it under the variable-catalogue issue (#446).
If there is a reason to prefer the narrowed-but-permissive form, that is a policy call rather than a data-hygiene one, and it should be stated explicitly in the PR body and the HISTORY entry so it is not read as an equivalent rewrite.
Everything else I found is non-blocking.
|
Non-blocking findings, plus the verification I did on the mechanics. The root cause survives this fix — filed as #446The 19 removed pack items were consumers of the defect, not the defect. The practical consequence is that Studio's variable picker still offers these to no-code users, so the first filter anyone builds on one reproduces #431 from the UI. Worth keeping in mind when #431 is closed: the pack items are fixed, the class is not. It also means the framing in the PR body is inverted for these six. It says the items reference "studio variables that exist in no module" and that "every load-bearing identifier is backed by nothing" — the variables exist, their fields do not. That is worth being precise about because it is the exact mirror of the Removed items leave their installed artifacts behind
The migration overwrites local edits without checking
MinorThe new Verified
One carry-overNeither this PR's CI nor #432's runs the assert that would actually validate this fix — |
…igration writes Per review on #444: - Remove GMI Residency Requirement instead of rewriting it: dropping the is_legal_resident AND-conjunct made the rule MORE permissive, the one fail-open change in the set. Rewrites are now 3, all strictly narrowing; removals are 20. - Guard every migration write (removals included) on the item still carrying the known-broken shipped expression, verified byte-for-byte against origin/19.0, so locally repaired items survive the upgrade untouched (logged). Rewrites update the cel_expression key inside logic_data instead of replacing the whole blob. - Scope in_institutional_care to individuals: variable applies_to and the restored item's context_type both narrow from both to individual (guarded migration write for existing databases). - Document that logic installed from removed items (installed_logic_id) is deliberately not deleted - deployment-owned data.
|
All points addressed — thank you for the review; the fail-open catch and the guard suggestion both made this materially safer. Blocker — GMI Residency Requirement: removed (your option 1). You're right that it was the one fail-open change dressed up as a narrowing rewrite: the other three drop OR-branches, this one dropped an AND-conjunct. Rewrites are now 3 (all strictly narrowing), removals 20. The PR body and HISTORY now state the fail-open rationale explicitly instead of listing it alongside the narrowing rewrites. Migration now guards every write — removals included. Each of the 20 removals and 3 rewrites only proceeds if the item still carries the known-broken shipped expression (verified byte-for-byte against
Installed artifacts: deliberately left alone, now documented. Framing fixed. The PR body now distinguishes the two defect shapes: identifiers with no |
emjay0921
left a comment
There was a problem hiding this comment.
Approved. The blocker is resolved the way I'd hoped — removal rather than a narrowed-but-permissive rewrite — and the rationale is now recorded in both the PR body and the HISTORY entry, so the next reader sees why that item is in the removed set and not the rewritten one.
I re-derived the claims rather than reading them:
- Every guard matches shipped data. I parsed the 106 pack items out of
origin/19.0's XML and compared each guard's expected expression against what actually shipped: 23/23 exact, zero mismatches. That was the failure mode worth checking — a guard whose expected string differed by a space would silently skip a broken item and leave it in place on upgrade, which is exactly the kind of bug that only shows up in production databases. - XML and migration agree. 20 items gone from the pack XML, 20 in
REMOVED_ITEMS, no drift in either direction; all 3 rewritten expressions identical between XML and migration. Fresh installs and upgraded databases converge on the same state. pack_gmi_item_residencyis gone from the pack XML and now carries its shipped expression as a removal guard. 1 fixed + 3 rewritten + 20 removed = 24.applies_to="individual"on the new variable, andcontext_type="individual"on the restored item in both the XML and the guarded migration write. Narrowing the item's context alongside the variable's scope is the right call and the reason is sound — the strict test picks its translation profile fromcontext_type, so leaving itbothwould probe an individual-scoped variable in group context.
Extending the guard to the removals was the better call, and it's a genuine improvement on what I suggested. I'd only argued for guarding the rewrites; you're right that a deployment which defined the missing variables itself would have working items, and deleting those would be worse than leaving broken ones. Mutating only the cel_expression key instead of replacing the blob closes the other half of it.
On installed artifacts — agreed, and no need to revisit. Not following installed_logic_id is the right default: those records were never evaluable so they can't have working consumers, and a mandatory migration shouldn't delete data the deployment owns. The residual exposure is bounded, too — ci-full builds fresh databases, so the strict suite never meets them there; only a long-lived deployment that installed one of the removed packs would, and as documented that's its own cleanup. Recording the decision in the docstring is what makes it reviewable later, which is the part that matters.
CI is green (34 checks; Trivy skipped as usual).
Merge order from here: this lands and closes #431 → #432 unblocks → a manual ci-full run is what actually proves test_all_pack_cel_expressions_translate passes on corrected data, since no per-module job installs spp_studio and spp_cel_load_testing together. #446 stays open for the variable catalogue behind all of this.
|
Post-merge verification verdict for the strict suite, with one correction to the merge-order note above.
So the verdict was produced directly: a local run on 19.0 @ 614cad7 with For the record, the ci-full failure itself is not related to this PR: diffing the three demo jobs' failure sets against the 2026-08-20 baseline run 32350162352, GRM and SP-MIS are identical to the known-red baseline, and DRIMS adds exactly the one known #438 test bug ( Follow-up worth considering: give ci-full (or a per-module job) a leg that installs |
Fixes #431.
Problem
24 of the 106
spp.studio.pack.itemrecords shipped inspp_studio/data/packs/carry filterexpressions that cannot work on any install: every load-bearing identifier ultimately points at a
registrant field that exists in no module — in some cases the identifier has no
spp.cel.variableat all, and in others the variable IS catalogued but its
source_fieldis dangling (the broadervariable-catalogue defect is #446; this PR fixes the shipped pack items that consume it). They also fail
spp_cel_load_testing's strict studio-validation test(
test_all_pack_cel_expressions_translate), which gates the merge of #432 and would fail theweekly
ci-fullrun once that migration lands.Approach
Per-item rewrite where a real near-equivalent survives, remove the rest — no placeholder
variables were invented, and no test was weakened. Every candidate rewrite was validated
empirically through the exact resolve+translate path the test uses (odoo shell against an
installed DB), because translation stops at the first bad identifier — terms after it in the
original expressions had never actually been validated.
Fixed properly (1)
Exclusion Criteria · Institutional Residence Exclusion keeps its shipped expression
(
!in_institutional_care): the item only lacked a variable definition — corespp_registryalready defines the
in_institutional_carefield on individuals. This PR adds the missingin_institutional_carestandard variable over that field.standard_variables.xmlis notnoupdate, so existing databases get the variable through the regular upgrade data load.Rewritten (3) — every rewrite is strictly narrowing
age < child_age_limit || (age < 25 && is_in_education)age < child_age_limitis_elderly_headed || is_child_headed || is_skipped_generation || has_chronically_ill_headis_elderly_headed || is_child_headedper_capita_income <= poverty_threshold || is_food_insecureper_capita_income <= poverty_thresholdAll three drop OR-branches, so the predicate only gets stricter — a template can lose reach, never
silently grant eligibility. Descriptions updated where the semantics narrowed. (GMI Residency
Requirement was initially rewritten here too, but that rewrite dropped an AND-conjunct —
fail-open — so per review it is removed instead.)
Removed (20)
No meaningful sub-expression survives — every load-bearing identifier is backed by nothing:
!has_mother && !has_fatherfallback expands tomembers.*, which doesn't exist in individual context — no rescue)is_single_parentalso fails validation; the single surviving term isn't a near-equivalent)is_legal_residentexists nowhere; droppingit would leave
residency_months >= 12alone, which is MORE permissive than the shipped rule —the one fail-open case in the set, so per review it is removed rather than rewritten)
Housing Quality, Pension Receipt, Duplicate Program, Income Tax Payer, Bank Balance,
Livestock Ownership (10 items; the pack keeps Land Ownership and Institutional Residence)
Migration
Pack data is
noupdate="1", so existing databases keep the broken records after the XML change.migrations/19.0.2.0.2/post-migration.pyapplies the same 20 removals and 3 rewrites directly.Per review, every migration write is guarded on the item still carrying the known-broken shipped
expression (verified byte-for-byte against
origin/19.0):noupdateexists so local changessurvive upgrades, so a deployment that already repaired an item (e.g. by defining the missing
variables itself) keeps its version, with a log line. The restored Institutional Residence item
needs no data fix — its new variable arrives via the regular (non-noupdate) data load — but the
migration narrows its
context_typefrombothtoindividualto match the variable's scope(also guarded). The new
in_institutional_carevariable usesapplies_to="individual"per review.Installed artifacts are deliberately left alone:
installed_logic_idpoints atspp.cel.expressionrecords copied at pack-install time; once installed they are the deployment'sown data and may be referenced by programs, so the migration does not follow and delete them. They
were never evaluable, and cleaning them up is the deployment's call. The decision is documented in
the migration docstring.
Version
19.0.2.0.1 → 19.0.2.0.2+ HISTORY fragment.Verification
All local (Docker), with
spp_cel_load_testingtaken from PR #432 @419752d6since the strict test lives there:test_all_pack_cel_expressions_translatefails with exactly the 24 items from spp_studio: shipped logic packs contain filter expressions referencing nonexistent fields/variables (24 items) #431 (14 translation errors + 10 unresolved-variable items).0 failed, 0 error(s) of 6 tests;Translated 30/25 CEL expressions (0 errors, 0 with unresolved variables).origin/19.0), with ONE item locally modified pre-upgrade to simulate an admin repair (Child Age Eligibility set toage < 18via SQL). The migration skips it with a warning and processes the rest:removed 20 and rewrote 2; item count 106 → 86; thein_institutional_carevariable is created by the data load withapplies_to=individual; the restored item lands asindividual | !in_institutional_care; the locally-repaired item keepsage < 18and still translates; strict suite0 failed of 6on the upgraded DB.Passed: 225 | Failed: 0 | Errors: 0.Out of scope / follow-ups
is_orphan,owns_car,land_hectares,residency_years,is_single_parent, …). They are the samedefect one level down but are not exercised by the strict test unless a pack item references
them; needs its own issue.
Disability Status Verification maps cleanly onto
spp_disability_registry(
has_disability,disability_severity_id, assessments), and livestock/land exclusions ontospp_farmer_registry/spp_land_record. Since pack items are plain data records, those modulescan ship their own items into the shipped packs, defined against their own fields — the right
long-term home for domain templates. Needs its own issue.
comparisons through unvalidated (
r.land_hectares <= 2translates even though the field existsnowhere) — latent quirk worth a look.
required_modulessupport for "template" packs (issue spp_studio: shipped logic packs contain filter expressions referencing nonexistent fields/variables (24 items) #431 Option 3) if aspirational packs arewanted as a product feature.
Reviewer caveat
Neither this PR's CI nor #432's exercises the strict test (per-module jobs don't co-install
spp_studioandspp_cel_load_testing). Only a manual ci-full run after both merge does.The local runs documented above are the substitute evidence.