From 56fdd8e5b99d9e92629b7db7944ec8f1ec71ff35 Mon Sep 17 00:00:00 2001 From: Jared Knowles Date: Tue, 28 Apr 2026 11:58:05 -0400 Subject: [PATCH] feat: export cog_basket_resolution() and cog_basket_unresolved() Sidecar accessors for basket-mode results. cog_basket_resolution() returns the full resolution tibble (drops candidates list-col by default for readable printing). cog_basket_unresolved() filters to ambiguous/no_match rows for iterative refinement. --- NAMESPACE | 2 ++ R/basket.R | 44 +++++++++++++++++++++++++++++++ man/cog_basket_resolution.Rd | 25 ++++++++++++++++++ man/cog_basket_unresolved.Rd | 20 ++++++++++++++ tests/testthat/test-basket.R | 51 ++++++++++++++++++++++++++++++++++++ 5 files changed, 142 insertions(+) create mode 100644 man/cog_basket_resolution.Rd create mode 100644 man/cog_basket_unresolved.Rd create mode 100644 tests/testthat/test-basket.R diff --git a/NAMESPACE b/NAMESPACE index 1550d74..518e06d 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,5 +1,7 @@ # Generated by roxygen2: do not edit by hand +export(cog_basket_resolution) +export(cog_basket_unresolved) export(cog_categories) export(cog_explain) export(cog_find_peers) diff --git a/R/basket.R b/R/basket.R index 0f9d91a..c6505cd 100644 --- a/R/basket.R +++ b/R/basket.R @@ -72,3 +72,47 @@ )) invisible(NULL) } + +#' Inspect basket-mode resolution sidecar +#' +#' Returns the resolution tibble attached to a basket-mode result of +#' [cog_gov_search()]. One row per input entry; `status` is one of +#' `"resolved"`, `"largest_pop"`, `"ambiguous"`, `"no_match"`. By default +#' the `candidates` list-column is dropped for readable printing; pass +#' `expand_candidates = TRUE` to keep it. +#' +#' @param x A tibble returned by basket-mode [cog_gov_search()]. +#' @param expand_candidates Logical. If `TRUE`, keeps the `candidates` +#' list-column (full-schema match candidates per input row). Default +#' `FALSE`. +#' @return A tibble with the resolution audit trail. +#' @export +cog_basket_resolution <- function(x, expand_candidates = FALSE) { + res <- attr(x, "resolution") + if (is.null(res)) { + cli::cli_abort(c( + "`x` has no resolution attribute.", + i = "Pass the result of basket-mode `cog_gov_search()` (length(name) > 1).", + i = "Single-name (utility) results do not carry a sidecar." + )) + } + if (!isTRUE(expand_candidates)) { + res$candidates <- NULL + } + res +} + +#' Filter a basket resolution to unresolved rows +#' +#' Convenience wrapper that returns just the rows where `status` is +#' `"ambiguous"` or `"no_match"` — the ones the user likely wants to +#' refine before piping into a query verb. The `candidates` list-column +#' is preserved so the user can drill into ambiguous match sets. +#' +#' @param x A tibble returned by basket-mode [cog_gov_search()]. +#' @return A tibble (subset of [cog_basket_resolution()]). +#' @export +cog_basket_unresolved <- function(x) { + res <- cog_basket_resolution(x, expand_candidates = TRUE) + res[res$status %in% c("ambiguous", "no_match"), , drop = FALSE] +} diff --git a/man/cog_basket_resolution.Rd b/man/cog_basket_resolution.Rd new file mode 100644 index 0000000..6ad2f24 --- /dev/null +++ b/man/cog_basket_resolution.Rd @@ -0,0 +1,25 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/basket.R +\name{cog_basket_resolution} +\alias{cog_basket_resolution} +\title{Inspect basket-mode resolution sidecar} +\usage{ +cog_basket_resolution(x, expand_candidates = FALSE) +} +\arguments{ +\item{x}{A tibble returned by basket-mode [cog_gov_search()].} + +\item{expand_candidates}{Logical. If `TRUE`, keeps the `candidates` +list-column (full-schema match candidates per input row). Default +`FALSE`.} +} +\value{ +A tibble with the resolution audit trail. +} +\description{ +Returns the resolution tibble attached to a basket-mode result of +[cog_gov_search()]. One row per input entry; `status` is one of +`"resolved"`, `"largest_pop"`, `"ambiguous"`, `"no_match"`. By default +the `candidates` list-column is dropped for readable printing; pass +`expand_candidates = TRUE` to keep it. +} diff --git a/man/cog_basket_unresolved.Rd b/man/cog_basket_unresolved.Rd new file mode 100644 index 0000000..727764c --- /dev/null +++ b/man/cog_basket_unresolved.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/basket.R +\name{cog_basket_unresolved} +\alias{cog_basket_unresolved} +\title{Filter a basket resolution to unresolved rows} +\usage{ +cog_basket_unresolved(x) +} +\arguments{ +\item{x}{A tibble returned by basket-mode [cog_gov_search()].} +} +\value{ +A tibble (subset of [cog_basket_resolution()]). +} +\description{ +Convenience wrapper that returns just the rows where `status` is +`"ambiguous"` or `"no_match"` — the ones the user likely wants to +refine before piping into a query verb. The `candidates` list-column +is preserved so the user can drill into ambiguous match sets. +} diff --git a/tests/testthat/test-basket.R b/tests/testthat/test-basket.R new file mode 100644 index 0000000..7ee1b4e --- /dev/null +++ b/tests/testthat/test-basket.R @@ -0,0 +1,51 @@ +test_that("cog_basket_resolution returns the sidecar tibble", { + basket <- suppressMessages(cog_gov_search( + name = c("Broward", "Notarealplace"), + state = c("FL", "NY") + )) + res <- cog_basket_resolution(basket) + expect_s3_class(res, "tbl_df") + expect_equal(nrow(res), 2L) + expect_setequal(colnames(res), c( + "query_name", "query_state", "query_type", "status", + "match_method", "canonical_govid", "gov_name", "n_candidates" + )) +}) + +test_that("cog_basket_resolution(expand_candidates = TRUE) keeps candidates list-col", { + basket <- suppressMessages(cog_gov_search( + name = c("Broward", "Notarealplace"), + state = c("FL", "NY") + )) + res <- cog_basket_resolution(basket, expand_candidates = TRUE) + expect_true("candidates" %in% colnames(res)) + expect_true(is.list(res$candidates)) +}) + +test_that("cog_basket_resolution errors on a non-basket tibble", { + utility <- cog_gov_search("BROWARD") + expect_error( + cog_basket_resolution(utility), + regexp = "no resolution attribute" + ) +}) + +test_that("cog_basket_unresolved filters to ambiguous and no_match", { + basket <- suppressMessages(cog_gov_search( + name = c("Broward", "San Diego", "Notarealplace"), + state = c("FL", "CA", "NY") + )) + unres <- cog_basket_unresolved(basket) + expect_equal(nrow(unres), 2L) + expect_setequal(unres$status, c("ambiguous", "no_match")) + expect_true("candidates" %in% colnames(unres)) +}) + +test_that("cog_basket_unresolved returns 0 rows when basket is clean", { + basket <- cog_gov_search( + name = c("BROWARD COUNTY", "SAN DIEGO CITY"), + state = c("FL", "CA") + ) + unres <- cog_basket_unresolved(basket) + expect_equal(nrow(unres), 0L) +})