fix: never suggest an intergovernmental (M/L) recipe as a Direct coverage-gap filler (I2)
.build_suggestions()'s candidate query picks recipes by component_code matching the requested category's summary_categories rows, with no flow-prefix filter. Task 1's M04/M05 category rows share the "Corrections" category with the Direct-flavored E04/E05, so corrections_ig_local_combined (entirely M-prefixed) became a raw top-level candidate for a plain (Direct) cog_spending() call. Following that hint would silently return intergovernmental dollars under provenance$expenditure_concept = "direct". Task 6's flow-family gate in .attach_ig_counterparts() already protects the *counterpart* lookup (deciding whether a firing suggestion gets an ig_recipe_id attached) but never touched the candidate list itself. Exclude any recipe with an M/L-prefixed component from candidates unconditionally -- an M/L recipe should never be a coverage-gap filler for either verb, which is a stronger guarantee than the counterpart gate's flow_prefixes check. Confirmed via the full suite: before this fix, a Direct cog_spending() call for category = "Corrections" printed "corrections_ig_local_combined ... re-run with recipe = 'corrections_ig_local_combined'" as its own suggestion; after, it appears only as the "intergovernmental counterpart" annotation on corrections_combined and its capital-outlay siblings. The pre-existing "IG Federal" mis-scoped test (revenue-side B-prefixed recipes) is unaffected -- those aren't M/L, so they remain valid candidates with ig_recipe_id still gated to NULL.
This commit is contained in:
+25
-1
@@ -22,6 +22,13 @@
|
||||
# positives from ordinary reporting variance -- most governments don't use
|
||||
# every sibling code in a multi-code category every year, and that is not
|
||||
# a format-boundary gap worth signposting.
|
||||
#
|
||||
# C1(a): for expenditure_concept = "total" callers, `result` here must
|
||||
# already be the Direct-leg subset (the caller filters out
|
||||
# spend_subtype == "intergovernmental" rows before calling in). A gap year
|
||||
# is "the requested year has no Direct rows", never "no rows at all" --
|
||||
# an IG row surviving on a legacy aggregate that Direct excludes must not
|
||||
# read as coverage and cancel the very suggestion that would recover it.
|
||||
|
||||
#' Build the `prov$suggestions` list for a (non-recipe) basis = "harmonized"
|
||||
#' verb call: recipes whose generic join would fill a real gap in `result`.
|
||||
@@ -33,7 +40,8 @@
|
||||
#' @param category `category` argument as passed to the verb (character
|
||||
#' vector or `NULL`; suggestions are only computed when non-NULL).
|
||||
#' @param result The verb's already-computed result tibble (post basis
|
||||
#' query, pre per_capita/adjust_to_year).
|
||||
#' query, pre per_capita/adjust_to_year), pre-filtered to the Direct leg
|
||||
#' only when the caller's `expenditure_concept = "total"` (see C1(a)).
|
||||
#' @param basis The *resolved* basis (`"harmonized"` or `"raw"`).
|
||||
#' @param flow_prefixes The calling verb's own flow-type prefixes (e.g.
|
||||
#' `c("E", "F", "G")` for `cog_spending()`, `c("T", "A", "U", "B", "C",
|
||||
@@ -47,10 +55,26 @@
|
||||
flow_prefixes) {
|
||||
if (!identical(basis, "harmonized") || is.null(category)) return(list())
|
||||
|
||||
# Exclude any recipe that is ITSELF an intergovernmental (M/L) recipe --
|
||||
# i.e. every one of its own component codes is M/L-prefixed. Without this,
|
||||
# a category whose summary_categories rows span both a Direct family
|
||||
# (e.g. E04/E05, "Corrections") and its M/L counterpart (M04/M05, same
|
||||
# category since Task 1) makes the M/L recipe itself (e.g.
|
||||
# `corrections_ig_local_combined`) a raw top-level candidate for a plain
|
||||
# (Direct) cog_spending() call -- following that hint would silently
|
||||
# return intergovernmental dollars under `expenditure_concept = "direct"`
|
||||
# provenance. This is a stronger, unconditional exclusion than the
|
||||
# flow-prefix gate below/in `.attach_ig_counterparts()`: an M/L recipe
|
||||
# should never be suggested as a coverage-gap filler for EITHER verb, not
|
||||
# just kept from being named as the *counterpart* of another suggestion.
|
||||
candidates <- DBI::dbGetQuery(con, sprintf(
|
||||
"SELECT DISTINCT recipe_id FROM harmonization_recipes
|
||||
WHERE component_code IN (
|
||||
SELECT DISTINCT item_code FROM summary_categories WHERE category IN (%s)
|
||||
)
|
||||
AND recipe_id NOT IN (
|
||||
SELECT DISTINCT recipe_id FROM harmonization_recipes
|
||||
WHERE LEFT(component_code, 1) IN ('M', 'L')
|
||||
)",
|
||||
.sql_lit_chr(category)
|
||||
))$recipe_id
|
||||
|
||||
Reference in New Issue
Block a user