feat: cog_spending + cog_revenue + cog_explain

Three core query verbs over the spending_annotated / revenue_annotated
DuckDB views. Each verb accepts vector govid, vector years, optional
category filter, per_capita flag, and adjust_to_year for CPI-U
real-dollar conversion (bundled index).

Amounts are returned in full USD (SUM(amt) * 1000) so callers can
freely rescale to millions/billions. The $1,000s -> $USD conversion
is recorded in provenance$transformations$units_conversion.

Every result carries an attr(., "provenance") list matching
inst/schemas/provenance-v1.json. cog_explain() prints the structured
form via cli or returns the raw list for MCP/JSON consumers.

Also: .fetch_or_cache_manifest() now handles local fixture paths so
tests can point USCOGDATA_FIXTURE_URL at the pipeline publish_cache/
without a working HTTP server.

Tests: 80 pass / 0 fail. devtools::check() 0E/0W/2N (both notes
pre-existing / environmental).
This commit is contained in:
2026-04-24 09:49:09 -04:00
parent cea7a241c5
commit c682e6547d
12 changed files with 672 additions and 1 deletions
+100
View File
@@ -0,0 +1,100 @@
# R/explain.R
#' Explain a verb result's provenance
#'
#' Prints the structured provenance attached to a tibble returned by any
#' `cog_*` verb, or returns it as a list for downstream use (MCP tools,
#' dashboards, JSON export).
#'
#' @param result A tibble returned by a `cog_*` verb.
#' @param format `"print"` (default) for a human-readable cli summary;
#' returns `result` invisibly for chaining. `"list"` returns the raw
#' provenance list (identical to `attr(result, "provenance")`).
#' @return Either `result` (invisibly) or the provenance list.
#' @export
cog_explain <- function(result, format = c("print", "list")) {
format <- match.arg(format)
prov <- attr(result, "provenance")
if (is.null(prov)) {
cli::cli_abort(c(
"No `provenance` attribute on result.",
i = "Pass a tibble returned by a cog_* verb (e.g. cog_spending())."
))
}
if (format == "list") return(prov)
.print_provenance(prov)
invisible(result)
}
#' @noRd
.print_provenance <- function(prov) {
cli::cli_h1("{prov$verb}()")
tgt_ids <- paste(prov$target$canonical_govid, collapse = ", ")
tgt_names <- if (length(prov$target$gov_name) == 0L) {
"(no rows returned)"
} else {
paste(prov$target$gov_name, collapse = ", ")
}
cli::cli_text("Target: {tgt_names} [canonical_govid: {tgt_ids}]")
yrs <- prov$years
cli::cli_text(if (length(yrs) == 1L) {
"Year: {yrs}"
} else {
"Years: {min(yrs)}-{max(yrs)} ({length(yrs)} years)"
})
if (!is.null(prov$category)) {
cli::cli_text("Category: {paste(prov$category, collapse = ', ')}")
} else {
cli::cli_text("Category: (all)")
}
cli::cli_h2("Codes observed")
codes <- prov$codes_summed$observed
if (length(codes) == 0L) {
cli::cli_alert_info("No item codes matched.")
} else {
cli::cli_ul(codes)
}
if (isTRUE(prov$aggregate_fallback$applied)) {
cli::cli_h2("Aggregate fallback")
cli::cli_alert_warning(
"Aggregate fallback used for years: {paste(prov$aggregate_fallback$years, collapse = ', ')}"
)
}
cli::cli_h2("Transformations")
uc <- prov$transformations$units_conversion
if (isTRUE(uc$applied)) {
cli::cli_text("Units: {uc$source_unit} -> {uc$target_unit} (x{uc$multiplier})")
}
pc <- prov$transformations$per_capita
if (isTRUE(pc$applied)) {
cli::cli_text("Per-capita denominator: {pc$denominator_source}")
}
infl <- prov$transformations$inflation
if (isTRUE(infl$applied)) {
cli::cli_text("Inflation: {infl$index}, base year {infl$base_year}")
}
cli::cli_h2("Scope")
cli::cli_text(
"Included gov types: {paste(prov$scope$gov_types_included, collapse = ', ')}"
)
cli::cli_text(
"Excluded gov types: {paste(prov$scope$gov_types_excluded, collapse = ', ')}"
)
if (nzchar(prov$scope$scope_note %||% "")) {
cli::cli_text("Note: {prov$scope$scope_note}")
}
cli::cli_h2("Data vintage")
cli::cli_text(
"Manifest schema v{prov$manifest$schema_version}, pipeline {prov$manifest$pipeline_commit}, built {prov$manifest$built_at}"
)
invisible(NULL)
}