diff --git a/DESCRIPTION b/DESCRIPTION index d7243be..956ff3c 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,8 +1,8 @@ Package: pensar Type: Package Title: LLM Wiki Engine -Version: 0.6.3.2 -Date: 2026-05-19 +Version: 0.6.3.3 +Date: 2026-06-03 Authors@R: c( person("Troy", "Hernandez", role = c("aut", "cre"), email = "troy@cornball.ai", diff --git a/NEWS.md b/NEWS.md index b814e6b..2932898 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,14 @@ +# pensar 0.6.3.3 (dev) + +## Bug fixes + +* `autoresearch()` no longer crashes when a search query returns zero + results. Tagging the results with `date`, `source`, `query`, and + `angle` assigned a length-1 value onto a 0-row data.frame and errored + with "replacement has 1 row, data has 0". The validator now builds all + columns at full length before filtering empty URLs, keeping `date` and + `source` aligned when rows are dropped. + # pensar 0.6.3.2 (dev) ## Changes diff --git a/R/autoresearch_steps.R b/R/autoresearch_steps.R index 6f9c442..6e32430 100644 --- a/R/autoresearch_steps.R +++ b/R/autoresearch_steps.R @@ -27,8 +27,8 @@ autoresearch_run_searches <- function(queries, search_backend, program, queries$query[[i]]) raw <- search_backend(queries$query[[i]], program$max_sources_per_round) df <- .validate_autoresearch_search_results(raw) - df$query <- queries$query[[i]] - df$angle <- queries$angle[[i]] + df$query <- rep.int(queries$query[[i]], nrow(df)) + df$angle <- rep.int(queries$angle[[i]], nrow(df)) results[[i]] <- df .ar_msg(verbose, " search ", i, "/", nrow(queries), ": ", nrow(df), " ", if (nrow(df) == 1L) "result" else "results") @@ -564,21 +564,16 @@ autoresearch_write_pages <- function(pages, vault, program, overwrite = TRUE, stop("search_backend() result missing column(s): ", paste(missing, collapse = ", "), call. = FALSE) } - out <- x[, required, drop = FALSE] - out$title <- as.character(out$title) - out$url <- as.character(out$url) - out$snippet <- as.character(out$snippet) + date <- if ("date" %in% names(x)) as.character(x$date) else rep("", nrow(x)) + source <- if ("source" %in% names(x)) as.character(x$source) else rep("", nrow(x)) + out <- data.frame( + title = as.character(x$title), + url = as.character(x$url), + snippet = as.character(x$snippet), + date = date, + source = source, + stringsAsFactors = FALSE) out <- out[nzchar(out$url),, drop = FALSE] - if ("date" %in% names(x)) { - out$date <- as.character(x$date) - } else { - out$date <- "" - } - if ("source" %in% names(x)) { - out$source <- as.character(x$source) - } else { - out$source <- "" - } rownames(out) <- NULL out } diff --git a/inst/tinytest/test_autoresearch.R b/inst/tinytest/test_autoresearch.R index 48d66e5..0ddb4bd 100644 --- a/inst/tinytest/test_autoresearch.R +++ b/inst/tinytest/test_autoresearch.R @@ -1069,3 +1069,29 @@ setTimeLimit(cpu = Inf, elapsed = Inf, transient = FALSE) expect_true(!is.null(res_timeout)) expect_equal(nrow(res_timeout$claims), 1L) unlink(v_timeout, recursive = TRUE) + +# A search query that returns zero results must not crash validation or the +# search loop (regression: scalar date/source/query assignment onto a 0-row +# data.frame errored with "replacement has 1 row, data has 0"). +empty_search <- function(query, n) { + data.frame(title = character(), url = character(), + snippet = character(), stringsAsFactors = FALSE) +} +val_empty <- pensar:::.validate_autoresearch_search_results(empty_search("q", 5L)) +expect_equal(nrow(val_empty), 0L) +expect_true(all(c("title", "url", "snippet", "date", "source") %in% + names(val_empty))) + +# The url filter must keep date/source aligned when it drops rows. +mixed <- data.frame(title = c("a", "b"), url = c("", "https://x.test"), + snippet = c("s1", "s2"), stringsAsFactors = FALSE) +val_mixed <- pensar:::.validate_autoresearch_search_results(mixed) +expect_equal(nrow(val_mixed), 1L) +expect_equal(val_mixed$url, "https://x.test") + +# The full search loop tags query/angle even when a query yields nothing. +q <- data.frame(query = "q", angle = "a", stringsAsFactors = FALSE) +loop_empty <- pensar:::autoresearch_run_searches( + q, empty_search, list(max_sources_per_round = 5L), verbose = FALSE) +expect_equal(nrow(loop_empty), 0L) +expect_true(all(c("query", "angle") %in% names(loop_empty)))