Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# AlertTools 1.2.0

## Review follow-up

- Preserve the legacy dengue-parameter fallback for Zika while retaining A92.8
in pipeline results. The modern lookup requires disease-specific coverage
and reports missing municipalities; a read-only operational audit is included.

## Refactoring baseline

- Added an inventory of the legacy public API and internal consumers.
Expand Down
10 changes: 8 additions & 2 deletions R/alert_functions.R
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,8 @@
#'@param dataini "notif" (default) or "sinpri"
#'@param writedb TRUE if it should write into the database, default is FALSE.
#'@param datasource Database connection; defaults to the legacy global `con`.
#' Zika retains the legacy dengue-parameter fallback with a warning. Use
#' `fetch_alert_parameters()` for strict disease-specific parameter lookup.
#'@param workers Positive number of workers. Defaults to portable serial execution.
#'@param seed Optional seed for stochastic nowcasting.
#'@param verbose Whether to emit progress messages.
Expand Down Expand Up @@ -297,8 +299,9 @@ pipe_infodengue <- function(cities, cid10="A90", datarelatorio, finalday = NULL,

# If cities is a vector of geocodes, the pipeline reads the parameters from the dataframe
if (class(cities) %in% c("integer","numeric")) {
pars_table <- fetch_alert_parameters(conn = datasource, geocodes = cities,
disease = cid10)
pars_table <- .fetch_alert_parameters_impl(
cities = cities, cid10 = .normalize_disease(cid10), datasource = datasource
)
if (isTRUE(verbose)) message("reading parameters from database")
} else { # if city contains data already
if(all(c("municipio_geocodigo","limiar_preseason",
Expand All @@ -312,6 +315,9 @@ pipe_infodengue <- function(cities, cid10="A90", datarelatorio, finalday = NULL,
call. = FALSE)
}

# Parameter provenance may be dengue, but cases and results keep the
# requested disease identity at the legacy compatibility boundary.
pars_table$cid10 <- .normalize_disease(cid10)
cidades <- pars_table$municipio_geocodigo
if (isTRUE(verbose)) message("running analysis for ", length(cidades), " municipalities")

Expand Down
23 changes: 20 additions & 3 deletions R/api.R
Original file line number Diff line number Diff line change
Expand Up @@ -280,15 +280,32 @@ fetch_alert_parameters <- function(conn, geocodes, disease = "dengue") {
geocodes <- .validate_geocodes(geocodes)
cid10 <- .normalize_disease(disease)
result <- .repo_parameters(conn, geocodes, cid10)
if (!nrow(result) || !all(geocodes %in% result$municipio_geocodigo)) {
stop("Alert parameters were not found for every requested geocode and disease.",
call. = FALSE)
missing_codes <- setdiff(geocodes, result$municipio_geocodigo)
if (length(missing_codes)) {
stop("Alert parameters were not found for disease ", cid10,
"; missing municipio_geocodigo: ", paste(missing_codes, collapse = ", "),
".", call. = FALSE)
}
.validate_unique_keys(result, c("municipio_geocodigo", "cid10"), "parameters")
result <- result[order(match(result$municipio_geocodigo, geocodes)), , drop = FALSE]
rownames(result) <- NULL
result
}

.alert_parameter_coverage <- function(conn, geocodes) {
geocodes <- .validate_geocodes(geocodes)
rows <- .repo_parameters(conn, geocodes)
count <- function(disease) vapply(geocodes, function(code) {
sum(rows$municipio_geocodigo == code & rows$cid10 == disease)
}, integer(1))
zika <- count("A92.8")
dengue <- count("A90")
data.frame(municipio_geocodigo = geocodes, zika_parameters = zika,
dengue_parameters = dengue,
status = ifelse(zika == 1L, "specific", ifelse(zika > 1L, "duplicate",
ifelse(dengue == 1L, "legacy_only", "missing"))))
}

#' Run the alert pipeline
#'
#' This experimental entry point performs computation only. It never reads from
Expand Down
40 changes: 40 additions & 0 deletions docs/refactoring/zika-transition.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Zika: parâmetros durante a transição

A API nova exige exatamente um parâmetro A92.8 por município. Ausências listam
os geocódigos e o CID-10; duplicidades são rejeitadas. Não há substituição por
dengue na interface nova.

O wrapper `pipe_infodengue()` mantém a política histórica de `read.parameters()`:
Zika usa parâmetros A90 e emite aviso, inclusive quando há parâmetros A92.8.
Na fronteira legada, os parâmetros são associados à doença solicitada para que
casos, resultados e histórico continuem identificados como A92.8. Parâmetros
fornecidos explicitamente pelo consumidor continuam sendo respeitados.

`tools/audit-zika-coverage.R` recebe um CSV com `municipio_geocodigo` e produz
um relatório por município: `specific`, `legacy_only`, `missing` ou `duplicate`.
Usa as configurações PG* e uma transação somente leitura. Exemplo, após instalar
a candidata e configurar o acesso ao banco de homologação:

```sh
Rscript tools/audit-zika-coverage.R municipios.csv cobertura-zika.csv
```

Os testes cobrem cobertura completa, parcial, ausente e duplicada em banco
descartável. Nenhum banco operacional foi consultado nesta entrega. O relatório
de cobertura real e a aprovação de troca da política legada continuam sendo
condições da migração do consumidor, não conclusões dos testes sintéticos.

## Pré-requisitos da auditoria em homologação

Na inspeção do repositório em 29/10/2026, não foram localizados configuração de
acesso à homologação nem CSV da população operacional de municípios. Os códigos
das fixtures não podem substituir esse recorte. As configurações PG* da CI
apontam para um banco descartável e não constituem acesso à homologação.

Para concluir a auditoria, o responsável pelo ambiente precisa disponibilizar o
mecanismo de acesso somente leitura e a lista de municípios. O CSV deve conter
`municipio_geocodigo` com códigos numéricos únicos de sete dígitos. O script
rejeita entradas incompletas/duplicadas, carrega a candidata quando executado na
raiz do projeto e recusa sobrescrever um relatório existente. A saída inclui
metadados de data UTC, versão, hash da entrada e número de municípios; não inclui
credenciais. Não registrar o consumidor como migrado enquanto essa evidência faltar.
4 changes: 3 additions & 1 deletion man/pipe_infodengue.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions tests/testthat/test-pipe-infodengue-firstday.R
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ test_that("pipe_infodengue preserves firstday defaults and historical overrides"
calls <- list()
climate_starts <- list()
local_mocked_bindings(
fetch_alert_parameters = function(conn, geocodes, disease) {
data.frame(municipio_geocodigo = geocodes)
.fetch_alert_parameters_impl = function(cities, cid10, datasource) {
data.frame(municipio_geocodigo = cities, cid10 = cid10)
},
fetch_climate = function(conn, geocodes, climate_vars, start_date, end_date) {
climate_starts[[length(climate_starts) + 1L]] <<- start_date
Expand Down
51 changes: 51 additions & 0 deletions tests/testthat/test-zika-transition.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
test_that("modern Zika lookup reports complete, partial and absent coverage", {
conn <- sqlite_fixture_connection(environment())
codes <- c(3304557, 3303302)
dengue <- parameter_fixture()[rep(1, 2), ]
dengue$municipio_geocodigo <- codes
DBI::dbWriteTable(conn, "parameters", dengue, overwrite = TRUE)
zika <- dengue
zika$cid10 <- "A92.8"
zika$limiar_epidemico <- c(30, 40)

expect_error(fetch_alert_parameters(conn, codes, "zika"),
"A92.8.*3304557, 3303302")
expect_identical(AlertTools:::.alert_parameter_coverage(conn, codes)$status,
c("legacy_only", "legacy_only"))
DBI::dbWriteTable(conn, "parameters", zika[1, ], append = TRUE)
expect_error(fetch_alert_parameters(conn, codes, "zika"), "A92.8.*3303302")
expect_identical(AlertTools:::.alert_parameter_coverage(conn, codes)$status,
c("specific", "legacy_only"))
DBI::dbWriteTable(conn, "parameters", zika[2, ], append = TRUE)
expect_equal(fetch_alert_parameters(conn, rev(codes), "zika")$limiar_epidemico,
c(40, 30))
DBI::dbWriteTable(conn, "parameters", zika[1, ], append = TRUE)
expect_error(fetch_alert_parameters(conn, codes, "zika"), "keys.*unique")
expect_identical(AlertTools:::.alert_parameter_coverage(conn, codes)$status,
c("duplicate", "specific"))
})

test_that("legacy Zika fallback preserves dengue rules and Zika result identity", {
conn <- sqlite_fixture_connection(environment())
notifications <- notification_fixture()
notifications$cid10_codigo <- "A92.8"
notifications[c("dt_notific", "dt_sin_pri", "dt_digita")] <-
lapply(notifications[c("dt_notific", "dt_sin_pri", "dt_digita")], as.numeric)
DBI::dbWriteTable(conn, "Notificacao", notifications, overwrite = TRUE)
withr::local_options(list(lifecycle_verbosity = "quiet"))

run <- function() pipe_infodengue(
3304557, cid10 = "A92.8", datarelatorio = 202106, nowcasting = "none",
firstday = as.Date("2020-12-20"), datasource = conn, completetail = 0
)
expect_warning(legacy <- run(), "Using dengue parameters")
expect_true(all(legacy[[1]]$data$CID10 == "A92.8"))
expect_true(all(attr(legacy, "parameters")$cid10 == "A92.8"))
zika <- parameter_fixture()
zika$cid10 <- "A92.8"
zika$limiar_epidemico <- 999
DBI::dbWriteTable(conn, "parameters", zika, append = TRUE)
expect_warning(with_specific <- run(), "Using dengue parameters")
expect_equal(with_specific, legacy)
expect_equal(fetch_alert_parameters(conn, 3304557, "zika")$limiar_epidemico, 999)
})
40 changes: 40 additions & 0 deletions tools/audit-zika-coverage.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Read-only operational audit. Connection settings are supplied through PG*.
args <- commandArgs(trailingOnly = TRUE)
if (length(args) != 2L || !nzchar(Sys.getenv("PGDATABASE"))) {
stop("Usage: PGDATABASE=... Rscript tools/audit-zika-coverage.R geocodes.csv report.csv")
}
input <- utils::read.csv(args[[1]])
if (!"municipio_geocodigo" %in% names(input)) {
stop("Input CSV must contain municipio_geocodigo.")
}
geocodes <- input$municipio_geocodigo
if (!is.numeric(geocodes) || !length(geocodes) || anyNA(geocodes) ||
any(!is.finite(geocodes) | geocodes != floor(geocodes)) ||
any(geocodes < 1000000 | geocodes > 9999999) || anyDuplicated(geocodes)) {
stop("Input must contain unique, nonmissing seven-digit numeric municipality codes.")
}
if (file.exists("DESCRIPTION") &&
identical(unname(read.dcf("DESCRIPTION")[1, "Package"]), "AlertTools")) {
devtools::load_all(quiet = TRUE)
}
if (!requireNamespace("RPostgres", quietly = TRUE)) stop("RPostgres must be installed.")
outputs <- c(args[[2]], paste0(args[[2]], c(".metadata.csv", ".sources.csv")))
if (any(file.exists(outputs))) stop("Output already exists; choose a new path to preserve audit evidence.")
conn <- DBI::dbConnect(RPostgres::Postgres())
tryCatch({
DBI::dbBegin(conn)
DBI::dbExecute(conn, "SET TRANSACTION READ ONLY")
report <- AlertTools:::.alert_parameter_coverage(conn, geocodes)
utils::write.csv(report, args[[2]], row.names = FALSE)
metadata <- data.frame(audited_at_utc = format(Sys.time(), tz = "UTC", usetz = TRUE),
package_version = as.character(packageVersion("AlertTools")),
input_md5 = unname(tools::md5sum(args[[1]])),
municipalities = length(geocodes), read_only = TRUE)
utils::write.csv(metadata, paste0(args[[2]], ".metadata.csv"), row.names = FALSE)
sources <- list.files("R", pattern = "\\.R$", full.names = TRUE)
if (length(sources)) {
utils::write.csv(data.frame(file = sources, md5 = unname(tools::md5sum(sources))),
paste0(args[[2]], ".sources.csv"), row.names = FALSE)
}
DBI::dbRollback(conn)
}, finally = DBI::dbDisconnect(conn))
Loading