feat: cog_recipes + recipe= + signposting suggestions

Adds cog_recipes() to list the curated harmonization_recipes catalog (24
recipes / schema_version >= 5), and a recipe= argument on cog_spending()/
cog_revenue() that runs a recipe's generic multi-code join instead of the
category view: SUM(amt * weight) across whichever component codes are
present for a (year, canonical_govid), scoped by gov_type_scope. The join
deliberately does not filter is_aggregate -- the wide era (<= 2011) exposes
these split families (corrections 04+05, IG *89/*47, U4- rents, etc.) ONLY
as aggregate rows, with leaf codes first appearing in 2012, so excluding
aggregates would zero out the wide-era half of every recipe. This is safe
by corpus construction: wide-era rows are aggregate-only, modern rows are
leaf-only, and every component is year-scoped, so there is no
double-counting. recipe= is mutually exclusive with category=; the result's
subtype column reads "recipe" and category reads the recipe's label.

Adds recipe-component-driven signposting: when a basis="harmonized" +
category query comes back with zero rows in a requested year, and a
harmonization recipe covering that category would actually produce rows
for this government in that year (via the same join .run_recipe() uses),
the recipe is surfaced in provenance$suggestions plus one
cli::cli_inform() message. This is deliberately keyed off recipe
components rather than harmonization_map's suggested_recipe_id column
(which is empty on every live row -- the wide era's split families are
NA-by-construction via aggregate exclusion, not an NA ruling to hang a
suggestion off of).

Also populates the previously-always-empty provenance$series_break_refs
(schema v5 only: series_breaks_pq rows whose fin_code is among the
observed codes and whose break_year falls in the requested span), and
extends cog_explain() with Basis/Harmonization/Recipe/Suggestions/Series
breaks sections.
This commit is contained in:
2026-07-18 23:33:02 -04:00
parent 7818cd2b1a
commit 4de915b557
15 changed files with 751 additions and 16 deletions
+22
View File
@@ -0,0 +1,22 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/recipes.R
\name{cog_recipes}
\alias{cog_recipes}
\title{List available harmonization recipes}
\usage{
cog_recipes(pattern = NULL)
}
\arguments{
\item{pattern}{Optional regex matched case-insensitively against
`recipe_id` or `label`.}
}
\value{
Tibble with columns `recipe_id`, `label`, `n_components`,
`year_min`, `year_max` (the min/max component year coverage), sorted by
`recipe_id`.
}
\description{
Recipes are multi-code cross-vintage series (see [cog_spending()]'s
`recipe` argument) catalogued in the corpus's `harmonization_recipes`
table. Use this to discover valid `recipe` ids.
}
+9 -1
View File
@@ -10,7 +10,8 @@ cog_revenue(
category = NULL,
per_capita = FALSE,
adjust_to_year = NULL,
basis = c("harmonized", "raw")
basis = c("harmonized", "raw"),
recipe = NULL
)
}
\arguments{
@@ -40,6 +41,13 @@ folding). On a corpus with `schema_version < 5` (no harmonization
tables), `basis` silently resolves to `"raw"` when left at its default
and the resolution is recorded in the provenance; explicitly passing
`basis = "harmonized"` on such a corpus aborts.}
\item{recipe}{Optional harmonization recipe id (see [cog_recipes()]) for
multi-code cross-vintage series that a 1:1 harmonized_code mapping
can't express (e.g. a wide-era aggregate that only splits into leaf
codes in the modern era). Mutually exclusive with `category`. The
result's subtype column reads `"recipe"` and `category` reads the
recipe's label. Requires `schema_version >= 5`.}
}
\value{
Tibble with columns `year`, `canonical_govid`, `gov_name`,
+9 -1
View File
@@ -10,7 +10,8 @@ cog_spending(
category = NULL,
per_capita = FALSE,
adjust_to_year = NULL,
basis = c("harmonized", "raw")
basis = c("harmonized", "raw"),
recipe = NULL
)
}
\arguments{
@@ -40,6 +41,13 @@ folding). On a corpus with `schema_version < 5` (no harmonization
tables), `basis` silently resolves to `"raw"` when left at its default
and the resolution is recorded in the provenance; explicitly passing
`basis = "harmonized"` on such a corpus aborts.}
\item{recipe}{Optional harmonization recipe id (see [cog_recipes()]) for
multi-code cross-vintage series that a 1:1 harmonized_code mapping
can't express (e.g. a wide-era aggregate that only splits into leaf
codes in the modern era). Mutually exclusive with `category`. The
result's subtype column reads `"recipe"` and `category` reads the
recipe's label. Requires `schema_version >= 5`.}
}
\value{
Tibble with columns `year`, `canonical_govid`, `gov_name`,