From 7913b0f664888a088258fc6165fccc542727d243 Mon Sep 17 00:00:00 2001 From: Jared Knowles Date: Mon, 27 Jul 2026 10:27:12 -0400 Subject: [PATCH] feat: record expenditure_concept in provenance and its JSON schema Always populated, never implicit, so a downstream artifact says which concept produced it. cog-api passes provenance through verbatim. --- R/provenance.R | 4 ++++ R/spending.R | 10 ++++++++++ inst/schemas/provenance-v1.json | 9 +++++++++ tests/testthat/test-expenditure-concept.R | 20 ++++++++++++++++++++ 4 files changed, 43 insertions(+) diff --git a/R/provenance.R b/R/provenance.R index bfd7a03..c4e52b4 100644 --- a/R/provenance.R +++ b/R/provenance.R @@ -6,6 +6,8 @@ per_capita, adjust_to_year, result, sql, subtype_col, basis = NA_character_, basis_note = NA_character_, + expenditure_concept = "direct", + expenditure_concept_note = NA_character_, harmonization = NULL, recipe = NULL, suggestions = list()) { manifest <- .uscogdata_env$manifest @@ -51,6 +53,8 @@ category = category, basis = basis, basis_note = basis_note, + expenditure_concept = expenditure_concept, + expenditure_concept_note = expenditure_concept_note, harmonization = harmonization %||% list( applied = FALSE, na_rows_excluded = 0L, na_amount_excluded = 0, note = NA_character_ diff --git a/R/spending.R b/R/spending.R index d4cf7ff..582cb18 100644 --- a/R/spending.R +++ b/R/spending.R @@ -220,6 +220,14 @@ cog_spending <- function(govid, years, category = NULL, suggestions <- .build_suggestions(con, govid, years, category, result, resolved$basis) } + # Determine expenditure_concept_note: only non-empty for "total", explains + # how the IG leg was assembled from legacy-era aggregates. + expenditure_concept_note_for_prov <- if (identical(expenditure_concept, "total")) { + "Total = Direct + intergovernmental (M to local govts + L to state govts). Legacy-era IG is assembled from aggregate-flagged rows, which are year-disjoint from their modern leaf components; the L-- family total is excluded." + } else { + NA_character_ + } + prov <- .build_provenance( verb = verb, call = call, @@ -233,6 +241,8 @@ cog_spending <- function(govid, years, category = NULL, subtype_col = subtype_col, basis = basis_for_prov, basis_note = basis_note_for_prov, + expenditure_concept = expenditure_concept, + expenditure_concept_note = expenditure_concept_note_for_prov, harmonization = harmonization, recipe = recipe_block, suggestions = suggestions diff --git a/inst/schemas/provenance-v1.json b/inst/schemas/provenance-v1.json index 4c040b7..7aa9a70 100644 --- a/inst/schemas/provenance-v1.json +++ b/inst/schemas/provenance-v1.json @@ -12,6 +12,15 @@ "category": { "type": ["string", "array", "null"] }, "basis": { "type": ["string", "null"] }, "basis_note": { "type": ["string", "null"] }, + "expenditure_concept": { + "type": "string", + "enum": ["direct", "total"], + "description": "Which spending concept produced this result. 'direct' is the government's own E/F/G spending; 'total' adds its intergovernmental payments (M to local governments, L to state governments). Only 'direct' is valid for results combined across governments." + }, + "expenditure_concept_note": { + "type": ["string", "null"], + "description": "How the intergovernmental leg was assembled; null for 'direct'." + }, "harmonization": { "type": "object" }, "recipe": { "type": ["object", "null"] }, "suggestions": { "type": "array" }, diff --git a/tests/testthat/test-expenditure-concept.R b/tests/testthat/test-expenditure-concept.R index 2c202ff..ed3accf 100644 --- a/tests/testthat/test-expenditure-concept.R +++ b/tests/testthat/test-expenditure-concept.R @@ -244,3 +244,23 @@ test_that("both cross-government verbs still accept the direct default", { category = "Police", years = 2019) ) }) + +test_that("provenance always records the expenditure concept", { + d <- cog_spending("010000226085", years = 2019, category = "Police") + t <- cog_spending("010000226085", years = 2019, category = "Police", + expenditure_concept = "total") + expect_equal(attr(d, "provenance")$expenditure_concept, "direct") + expect_equal(attr(t, "provenance")$expenditure_concept, "total") + # The note explains the non-obvious part: how legacy IG was assembled. + expect_true(nzchar(attr(t, "provenance")$expenditure_concept_note)) + expect_true(is.na(attr(d, "provenance")$expenditure_concept_note) || + !nzchar(attr(d, "provenance")$expenditure_concept_note)) +}) + +test_that("the provenance schema documents expenditure_concept", { + sch <- jsonlite::fromJSON( + system.file("schemas", "provenance-v1.json", package = "uscogdata"), + simplifyVector = FALSE + ) + expect_true("expenditure_concept" %in% names(sch$properties)) +})