From 16d4a4b44aacda08d21270680e1414ec8cef7fef Mon Sep 17 00:00:00 2001 From: sdairs Date: Fri, 2 Oct 2026 16:42:12 +0100 Subject: [PATCH 1/4] Add transactional R and Shiny poll workbench --- .github/workflows/poll-studio.yml | 39 + applications/poll-studio/.R-version | 1 + applications/poll-studio/.Rprofile | 1 + applications/poll-studio/.env.example | 8 + applications/poll-studio/.gitignore | 8 + applications/poll-studio/R/database.R | 19 + applications/poll-studio/R/polls.R | 80 + applications/poll-studio/R/validation.R | 56 + applications/poll-studio/README.md | 189 ++ applications/poll-studio/app.R | 142 ++ applications/poll-studio/renv.lock | 2112 +++++++++++++++++++ applications/poll-studio/renv/.gitignore | 7 + applications/poll-studio/renv/activate.R | 1451 +++++++++++++ applications/poll-studio/renv/settings.json | 22 + applications/poll-studio/run.R | 7 + applications/poll-studio/sql/bootstrap.sql | 22 + applications/poll-studio/sql/cleanup.sql | 6 + applications/poll-studio/sql/migrate.sql | 47 + applications/poll-studio/sql/seed.sql | 13 + applications/poll-studio/test/browser.mjs | 207 ++ applications/poll-studio/test/cloud.R | 226 ++ applications/poll-studio/test/domain.R | 42 + applications/poll-studio/test/observe.R | 8 + applications/poll-studio/test/worker.R | 22 + applications/poll-studio/www/style.css | 36 + 25 files changed, 4771 insertions(+) create mode 100644 .github/workflows/poll-studio.yml create mode 100644 applications/poll-studio/.R-version create mode 100644 applications/poll-studio/.Rprofile create mode 100644 applications/poll-studio/.env.example create mode 100644 applications/poll-studio/.gitignore create mode 100644 applications/poll-studio/R/database.R create mode 100644 applications/poll-studio/R/polls.R create mode 100644 applications/poll-studio/R/validation.R create mode 100644 applications/poll-studio/README.md create mode 100644 applications/poll-studio/app.R create mode 100644 applications/poll-studio/renv.lock create mode 100644 applications/poll-studio/renv/.gitignore create mode 100644 applications/poll-studio/renv/activate.R create mode 100644 applications/poll-studio/renv/settings.json create mode 100644 applications/poll-studio/run.R create mode 100644 applications/poll-studio/sql/bootstrap.sql create mode 100644 applications/poll-studio/sql/cleanup.sql create mode 100644 applications/poll-studio/sql/migrate.sql create mode 100644 applications/poll-studio/sql/seed.sql create mode 100644 applications/poll-studio/test/browser.mjs create mode 100644 applications/poll-studio/test/cloud.R create mode 100644 applications/poll-studio/test/domain.R create mode 100644 applications/poll-studio/test/observe.R create mode 100644 applications/poll-studio/test/worker.R create mode 100644 applications/poll-studio/www/style.css diff --git a/.github/workflows/poll-studio.yml b/.github/workflows/poll-studio.yml new file mode 100644 index 00000000..a8888b17 --- /dev/null +++ b/.github/workflows/poll-studio.yml @@ -0,0 +1,39 @@ +name: Poll Studio checks + +on: + pull_request: + paths: + - 'applications/poll-studio/**' + - '.github/workflows/poll-studio.yml' + push: + branches: [main] + paths: + - 'applications/poll-studio/**' + - '.github/workflows/poll-studio.yml' + workflow_dispatch: + +permissions: + contents: read + +jobs: + check: + runs-on: ubuntu-latest + defaults: + run: + working-directory: applications/poll-studio + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: r-lib/actions/setup-r@f9a764fea8d5c63df6ef9a5c7795bf7deb5d7e05 # v2 + with: + r-version: '4.6.1' + use-public-rspm: false + - name: Install native package and graphics prerequisites + run: | + sudo apt-get update + sudo apt-get install -y libpq-dev libcurl4-openssl-dev libssl-dev libxml2-dev \ + libuv1-dev libcairo2-dev libpango1.0-dev libpng-dev libjpeg-dev libtiff-dev cmake + - run: Rscript -e 'renv::restore(prompt = FALSE)' + - run: Rscript -e 'stopifnot(capabilities("png"), capabilities("cairo")); for (p in c("app.R", "run.R", list.files("R", full.names=TRUE), list.files("test", pattern="\\.R$", full.names=TRUE))) parse(p)' + - run: Rscript test/domain.R + +# Dedicated Cloud and browser checks run separately without CI credentials. diff --git a/applications/poll-studio/.R-version b/applications/poll-studio/.R-version new file mode 100644 index 00000000..8ac28bf9 --- /dev/null +++ b/applications/poll-studio/.R-version @@ -0,0 +1 @@ +4.6.1 diff --git a/applications/poll-studio/.Rprofile b/applications/poll-studio/.Rprofile new file mode 100644 index 00000000..81b960f5 --- /dev/null +++ b/applications/poll-studio/.Rprofile @@ -0,0 +1 @@ +source("renv/activate.R") diff --git a/applications/poll-studio/.env.example b/applications/poll-studio/.env.example new file mode 100644 index 00000000..0ca2949c --- /dev/null +++ b/applications/poll-studio/.env.example @@ -0,0 +1,8 @@ +PGHOST=your-cloud-hostname +PGPORT=5432 +PGDATABASE=postgres +PGUSER=polls_app +PGPASSWORD=your-runtime-role-password +PGSSLROOTCERT=/absolute/path/cloud-ca.pem +APP_ORIGIN=http://127.0.0.1:4000 +PORT=4000 diff --git a/applications/poll-studio/.gitignore b/applications/poll-studio/.gitignore new file mode 100644 index 00000000..e16a6fea --- /dev/null +++ b/applications/poll-studio/.gitignore @@ -0,0 +1,8 @@ +.env +*.pem +.Rhistory +.RData +renv/library/ +renv/staging/ +renv/cache/ +node_modules/ diff --git a/applications/poll-studio/R/database.R b/applications/poll-studio/R/database.R new file mode 100644 index 00000000..fd765d0f --- /dev/null +++ b/applications/poll-studio/R/database.R @@ -0,0 +1,19 @@ +required_env <- function(name) { + value <- Sys.getenv(name, unset = "") + if (!nzchar(value)) stop(paste("Missing configuration:", name), call. = FALSE) + value +} + +connection_options <- function() { + list(drv = RPostgres::Postgres(), host = required_env("PGHOST"), + port = required_env("PGPORT"), dbname = required_env("PGDATABASE"), + user = required_env("PGUSER"), password = required_env("PGPASSWORD"), + sslmode = "verify-full", sslrootcert = required_env("PGSSLROOTCERT"), + connect_timeout = "10", bigint = "character", timezone = "UTC", + application_name = "poll-studio", + options = "-c statement_timeout=5000 -c lock_timeout=3000 -c idle_in_transaction_session_timeout=5000") +} + +new_pool <- function() { + do.call(pool::dbPool, c(connection_options(), list(minSize = 1L, maxSize = 4L))) +} diff --git a/applications/poll-studio/R/polls.R b/applications/poll-studio/R/polls.R new file mode 100644 index 00000000..29864daf --- /dev/null +++ b/applications/poll-studio/R/polls.R @@ -0,0 +1,80 @@ +create_poll <- function(db, question, choices) { + input <- validate_poll(question, choices) + pool::poolWithTransaction(db, function(conn) { + row <- DBI::dbGetQuery(conn, + "INSERT INTO poll_studio.polls (question) VALUES ($1) RETURNING id::text AS id", + params = list(input$question)) + for (position in seq_along(input$choices)) { + DBI::dbExecute(conn, + "INSERT INTO poll_studio.choices (poll_id, position, label) VALUES ($1::bigint, $2, $3)", + params = list(row$id[[1]], position, input$choices[[position]])) + } + row$id[[1]] + }) +} + +list_polls <- function(db) { + DBI::dbGetQuery(db, + "SELECT id::text AS id, question, closed_at IS NOT NULL AS closed, response_count, + created_at FROM poll_studio.polls ORDER BY created_at DESC, id DESC LIMIT 100") +} + +poll_summary <- function(db, poll_id) { + poll_id <- identifier(poll_id) + DBI::dbGetQuery(db, + "SELECT p.id::text AS poll_id, p.question, p.closed_at IS NOT NULL AS closed, + p.response_count, c.id::text AS choice_id, c.label, c.position, + count(r.id)::integer AS responses + FROM poll_studio.polls p JOIN poll_studio.choices c ON c.poll_id = p.id + LEFT JOIN poll_studio.responses r ON r.poll_id = c.poll_id AND r.choice_id = c.id + WHERE p.id = $1::bigint + GROUP BY p.id, c.id ORDER BY c.position", params = list(poll_id)) +} + +record_response <- function(db, poll_id, choice_id, code) { + poll_id <- identifier(poll_id) + choice_id <- identifier(choice_id) + code <- participant_code(code) + pool::poolWithTransaction(db, function(conn) { + poll <- DBI::dbGetQuery(conn, + "SELECT closed_at IS NOT NULL AS closed, response_count FROM poll_studio.polls + WHERE id = $1::bigint FOR UPDATE", params = list(poll_id)) + if (nrow(poll) == 0L) poll_error("Poll does not exist.", "missing") + retained <- DBI::dbGetQuery(conn, + "SELECT id::text AS id, choice_id::text AS choice_id FROM poll_studio.responses + WHERE poll_id = $1::bigint AND participant_code = $2", params = list(poll_id, code)) + if (nrow(retained) > 0L) { + if (!identical(retained$choice_id[[1]], choice_id)) { + poll_error("This code already selected a different choice.", "conflict") + } + return(list(id = retained$id[[1]], replayed = TRUE)) + } + if (poll$closed[[1]]) poll_error("Poll is closed; matching saved responses can still replay.", "conflict") + if (poll$response_count[[1]] >= 10000L) poll_error("This poll reached its 10,000-response limit.", "conflict") + choice <- DBI::dbGetQuery(conn, + "SELECT id FROM poll_studio.choices WHERE poll_id = $1::bigint AND id = $2::bigint", + params = list(poll_id, choice_id)) + if (nrow(choice) == 0L) poll_error("Choice does not belong to this poll.") + DBI::dbExecute(conn, + "UPDATE poll_studio.polls SET response_count = response_count + 1 WHERE id = $1::bigint", + params = list(poll_id)) + row <- DBI::dbGetQuery(conn, + "INSERT INTO poll_studio.responses (poll_id, choice_id, participant_code) + VALUES ($1::bigint, $2::bigint, $3) RETURNING id::text AS id", + params = list(poll_id, choice_id, code)) + list(id = row$id[[1]], replayed = FALSE) + }) +} + +close_poll <- function(db, poll_id) { + poll_id <- identifier(poll_id) + pool::poolWithTransaction(db, function(conn) { + row <- DBI::dbGetQuery(conn, + "SELECT id FROM poll_studio.polls WHERE id = $1::bigint FOR UPDATE", params = list(poll_id)) + if (nrow(row) == 0L) poll_error("Poll does not exist.", "missing") + DBI::dbExecute(conn, + "UPDATE poll_studio.polls SET closed_at = COALESCE(closed_at, clock_timestamp()) + WHERE id = $1::bigint", params = list(poll_id)) + invisible(TRUE) + }) +} diff --git a/applications/poll-studio/R/validation.R b/applications/poll-studio/R/validation.R new file mode 100644 index 00000000..a923bb65 --- /dev/null +++ b/applications/poll-studio/R/validation.R @@ -0,0 +1,56 @@ +poll_error <- function(message, kind = "validation") { + stop(structure(list(message = message, call = NULL), + class = c(paste0("poll_", kind), "poll_error", "error", "condition"))) +} + +bounded_text <- function(value, maximum, field) { + if (!is.character(value) || length(value) != 1L || is.na(value) || + grepl("[[:cntrl:]]", value)) { + poll_error(paste(field, "must be text without control characters.")) + } + value <- trimws(value) + if (nchar(value, type = "chars") < 1L || nchar(value, type = "chars") > maximum) { + poll_error(paste(field, "is outside its length limit.")) + } + value +} + +validate_poll <- function(question, choices) { + question <- bounded_text(question, 200L, "Question") + if (!is.character(choices) || length(choices) < 2L || length(choices) > 8L) { + poll_error("Use two through eight choices.") + } + choices <- vapply(choices, bounded_text, character(1), maximum = 80L, field = "Choice") + if (anyDuplicated(choices)) poll_error("Choice labels must be distinct after trimming.") + list(question = question, choices = unname(choices)) +} + +identifier <- function(value) { + if (!is.character(value) || length(value) != 1L || is.na(value) || + !grepl("^[1-9][0-9]{0,18}$", value) || + (nchar(value) == 19L && value > "9223372036854775807")) { + poll_error("Choose a valid stored poll or choice.") + } + value +} + +participant_code <- function(value) { + value <- toupper(bounded_text(value, 32L, "Participant code")) + if (!grepl("^[A-Z0-9][A-Z0-9_-]{2,31}$", value)) { + poll_error("Use a synthetic code of 3–32 letters, digits, underscores or hyphens.") + } + value +} + +trusted_origin <- function(request, expected) { + origin <- request$HTTP_ORIGIN + is.character(origin) && length(origin) == 1L && !is.na(origin) && identical(origin, expected) +} + +choice_lines <- function(value) { + if (!is.character(value) || length(value) != 1L || is.na(value) || + nchar(value, type = "chars") > 1024L) { + poll_error("Choice input must be at most 1,024 characters.") + } + strsplit(value, "\n", fixed = TRUE)[[1]] +} diff --git a/applications/poll-studio/README.md b/applications/poll-studio/README.md new file mode 100644 index 00000000..ad33866e --- /dev/null +++ b/applications/poll-studio/README.md @@ -0,0 +1,189 @@ +# Poll studio + +A trusted local R/Shiny workbench creates a fictional poll, records synthetic responses, draws stored counts, refreshes another browser's changes and closes the poll. ClickHouse Managed Postgres (public beta) stores the durable poll, choices and responses. Participant codes deduplicate submissions; they are not authenticated identities. All operators share authority. Use made-up labels/codes and no personal information. + +## Boundaries and behavior + +- Create stores a question and two through eight distinct choices in one transaction. Questions allow 1–200 characters, choices 1–80 after trimming; control characters are rejected. The multiline form input is capped at 1,024 characters. +- Respond locks the poll row, checks a saved poll/code response first, then open state, the 10,000-response application cap and the choice. It increments the counter and inserts the response using the same transaction connection. +- Matching retries return the original response, including after closure or the response cap. A different choice under the same normalized code conflicts. Codes are trimmed and uppercased, with 3–32 ASCII letters, digits, underscores or hyphens. +- Close takes the same parent lock. Counts query the stored rows; reactive values only invalidate reads. Refresh explicitly loads another browser's committed changes; there is no live push. +- The list contains the newest 100 polls ordered by creation time and ID; no pagination. A summary returns at most eight grouped counts, not all participant codes or response records. Charts show counts, without inference claims. + +The composite response foreign key enforces poll/choice membership. A unique poll/code constraint backs repeat safety. Runtime grants deny edits/deletes to existing choices/responses and deny DDL. A trusted holder of runtime database credentials can still append choices directly and update the poll counter or closed timestamp; complete choice-set creation, monotonic closure and the true response cap depend on the application's transaction/locking protocol. The role is shared and is not a per-user security boundary. This example does not claim a publicly writable survey service. + +The standalone Shiny server checks `session$request$HTTP_ORIGIN` from the actual WebSocket request against a fixed configured loopback origin before registering database observers. Missing/foreign Origins close the session. The trusted value is not derived from Host. This is browser cross-site protection, not authentication or a deployment access-control replacement. + +## Native prerequisites and restore + +Pinned runtime: R 4.6.1. The complete `renv.lock` pins Shiny 1.14.0, httpuv 1.6.17, DBI 1.3.0, RPostgres 1.4.10, pool 1.0.5 and their dependencies. Standard `.Rprofile`, `renv/activate.R` and settings bootstrap the project; package libraries and caches are not committed. renv does not install R or OS libraries. + +On Ubuntu 24.04 ARM64, install native build/client/graphics dependencies before compiling R or restoring packages: + +```bash +sudo apt-get update +sudo apt-get install -y build-essential gfortran curl ca-certificates \ + libcurl4-openssl-dev libssl-dev libxml2-dev libpq-dev libreadline-dev \ + libpcre2-dev libbz2-dev liblzma-dev zlib1g-dev libuv1-dev \ + libcairo2-dev libpango1.0-dev libpng-dev libjpeg-dev libtiff-dev cmake \ + postgresql-client xz-utils +``` + +Install the pinned R release using your supported runtime installer or compile the official source. A native prefix avoids changing another runtime: + +```bash +curl -fSLO https://cran.r-project.org/src/base/R-4/R-4.6.1.tar.gz +tar -xzf R-4.6.1.tar.gz +cd R-4.6.1 +./configure --prefix="$HOME/R" --with-x=no --enable-R-shlib +make -j2 +make install +export PATH="$HOME/R/bin:$PATH" +``` + +From this application directory: + +```bash +Rscript -e 'renv::restore(prompt = FALSE)' +Rscript -e 'stopifnot(capabilities("png"), capabilities("cairo"))' +Rscript test/domain.R +Rscript -e 'for (p in c("app.R", "run.R", list.files("R", full.names=TRUE))) parse(p)' +``` + +To prove restore into a separate fresh project/library/cache, initialize the project before restoring its lock. Initialization activates renv's isolated library/sandbox; an uninitialized `--vanilla` session can otherwise consider global packages sufficient. + +```bash +export RESTORE_PROJECT=/absolute/path/fresh-project +export SOURCE_LOCK="$PWD/renv.lock" +RENV_PATHS_CACHE=/absolute/path/fresh-cache Rscript --vanilla -e ' + project <- Sys.getenv("RESTORE_PROJECT") + renv::init(project=project, bare=TRUE) + file.copy(Sys.getenv("SOURCE_LOCK"), file.path(project, "renv.lock"), overwrite=TRUE) + renv::restore(project=project, prompt=FALSE) + for (p in c("shiny", "DBI", "RPostgres", "pool")) loadNamespace(p)' +``` + +## Create a dedicated Cloud service + +Use authenticated clickhousectl. Verify region/shape availability and costs before provisioning. The example uses AWS us-east-1, c6gd.large, PostgreSQL18 and no HA: + +```bash +export ORG_ID=your-organization-id +clickhousectl cloud postgres create --org-id "$ORG_ID" \ + --name poll-studio --provider aws --region us-east-1 --size c6gd.large \ + --pg-version 18 --ha-type none --json > /private/path/service.json +chmod 600 /private/path/service.json +export SERVICE_ID=your-returned-service-id +clickhousectl cloud postgres get "$SERVICE_ID" --org-id "$ORG_ID" --json +``` + +Repeat get until the state is running; retain the create receipt's credentials privately. Download the service CA: + +```bash +clickhousectl cloud postgres certs get "$SERVICE_ID" --org-id "$ORG_ID" \ + --output /private/path/cloud-ca.pem +``` + +Running services incur charges. No analytical ClickHouse service is used. + +## Empty-schema setup and seed + +Create a private setup environment file with the receipt's administrator credentials and distinct random role passwords of at least 24 characters: + +```dotenv +PGHOST=your-service-hostname +PGPORT=5432 +PGDATABASE=postgres +PGSSLMODE=verify-full +PGSSLROOTCERT=/absolute/path/cloud-ca.pem +ADMIN_USER=receipt-username +ADMIN_PASSWORD=receipt-password +MIGRATION_PASSWORD=distinct-random-owner-password +APP_PASSWORD=distinct-random-runtime-password +``` + +Export the fields, bootstrap as administrator, and migrate/seed as schema owner: + +```bash +set -a; source /private/path/setup.env; set +a +export PGUSER="$ADMIN_USER" PGPASSWORD="$ADMIN_PASSWORD" +psql -X -f sql/bootstrap.sql +export PGUSER=polls_migration PGPASSWORD="$MIGRATION_PASSWORD" +psql -X -f sql/migrate.sql +psql -X -f sql/seed.sql +``` + +Bootstrap runs once and creates the owned schema explicitly; the owner needs no database CREATE. It revokes PUBLIC database CREATE/TEMP and public-schema CREATE for this dedicated database. Migration version1 creates identity keys, constraints/indexes and scoped grants. Repeat migration and seed to check reproduction. The seed adds a fictional workshop poll without responses. Runtime receives select/insert, sequence usage and updates only to `response_count`/`closed_at`; no question edit, row delete, response/choice edit or schema-history access. Startup performs no migration or seed. + +## Start the local workbench + +Create a private runtime file from [.env.example](.env.example), with only runtime credentials and connection/origin fields: + +```bash +set -a; source /private/path/runtime.env; set +a +unset ADMIN_USER ADMIN_PASSWORD MIGRATION_PASSWORD APP_PASSWORD +export LANG=C.UTF-8 # Tested Ubuntu Unicode locale +Rscript run.R +``` + +Open exactly `http://127.0.0.1:4000`, matching `APP_ORIGIN`. The listener binds loopback. A different port requires matching PORT and fixed loopback APP_ORIGIN. Database options always use libpq `sslmode=verify-full`, the downloaded CA, a hostname, 10-second connect timeout, 5-second statement timeout and 3-second lock timeout. Bigint IDs map to character values, so they never pass through imprecise doubles; bounded counts are cast to integer. + +The finite pool holds one through four connections and closes on app stop. Every multi-write operation uses `pool::poolWithTransaction(db, function(conn) ...)` and only that callback connection. No connection or transaction remains checked out across browser input. Normal R database work in one Shiny process is synchronous; two sessions alone do not prove independent transaction contention. + +Expected validation/conflict errors appear as understandable notifications. Unexpected errors stay generic; Shiny error sanitization remains enabled. Text/radio/table labels use framework escaping. The chart renders horizontal bars natively with PNG/Cairo and abbreviates labels beyond 18 characters; the table retains full labels. This is a loopback demonstration, not a public deployment recipe; deployed use needs authentication, access controls, HTTPS and operational limits. + +## Acceptance against the dedicated Cloud fixture + +After setup, switch to runtime credentials but retain owner/admin fields only in this test shell: + +```bash +export PGUSER=polls_app PGPASSWORD="$APP_PASSWORD" +export EVIDENCE_DIR=/private/path/evidence +mkdir -p "$EVIDENCE_DIR" +Rscript test/cloud.R +``` + +The suite uses separate R worker processes for same-code contention and held-lock response/close ordering, observing actual blocked database sessions. Workers use longer timeouts only for controlled test barriers. Owner-only temporary rejecting triggers force failures after earlier writes, then are removed; no runtime privilege changes are needed. The cap test accelerates the stored counter with the owner and restores it afterward. Foreign-choice and duplicate-key checks exercise actual database constraints. TLS tests use a valid unrelated CA and resolve the real endpoint address with a deliberately mismatched libpq hostname, then prove the ordinary connection succeeds. + +Optional browser acceptance uses isolated Node24.21.0, Playwright1.63.0 and ws8.22.0 dependencies: + +```bash +export BROWSER_DIR=/private/path/browser-dependencies +mkdir -p "$BROWSER_DIR" +npm --prefix "$BROWSER_DIR" install --save-exact playwright@1.63.0 ws@8.22.0 +"$BROWSER_DIR/node_modules/.bin/playwright" install --with-deps chromium +cp test/browser.mjs "$BROWSER_DIR/browser.mjs" +export APP_DIR="$PWD" EVIDENCE_DIR=/private/path/evidence +# Use runtime.env only; the harness does not receive elevated credentials. +node "$BROWSER_DIR/browser.mjs" +``` + +It exercises two real contexts, create/respond/chart/refresh/close, replay/conflict, second-poll selection, escaped labels, eight maximum-length labels on mobile, a foreign-browser Origin and a missing-Origin WebSocket with independent stored-row observations, and confirmed original-process exit/restart. Evidence stays outside the source tree. + +## Cleanup + +Stop the app first (Ctrl+C). A destructive dedicated-fixture reset explicitly restores administrator credentials: + +```bash +set -a; source /private/path/setup.env; set +a +export PGUSER="$ADMIN_USER" PGPASSWORD="$ADMIN_PASSWORD" +psql -X -f sql/cleanup.sql +``` + +Cleanup removes only the application schema and two roles; PUBLIC privilege revocations remain. No production downgrade is supplied. Delete your billable fixture by its recorded exact ID and verify absence after asynchronous deletion: + +```bash +clickhousectl cloud postgres delete "$SERVICE_ID" --org-id "$ORG_ID" +clickhousectl cloud postgres list --org-id "$ORG_ID" --json +``` + +Deletion destroys that service's data; preserve needed evidence first. + +## Primary references + +- [One-connection pool transaction callback](https://rstudio.github.io/pool/reference/poolWithTransaction.html) +- [RPostgres libpq options and bigint mapping](https://rpostgres.r-dbi.org/reference/Postgres.html) +- [renv restore and runtime/system dependency limits](https://rstudio.github.io/renv/articles/renv.html) +- [Shiny1.14.0 session WebSocket request source](https://github.com/rstudio/shiny/blob/v1.14.0/R/shiny.R) +- [Standalone Shiny runApp](https://shiny.posit.co/r/reference/shiny/latest/runapp.html) +- [ClickHouse Managed Postgres setup](https://clickhouse.com/docs/products/managed-postgres/quickstart) diff --git a/applications/poll-studio/app.R b/applications/poll-studio/app.R new file mode 100644 index 00000000..8a8e175c --- /dev/null +++ b/applications/poll-studio/app.R @@ -0,0 +1,142 @@ +library(shiny) +source("R/validation.R") +source("R/database.R") +source("R/polls.R") + +port <- suppressWarnings(as.integer(Sys.getenv("PORT", "4000"))) +if (is.na(port) || port < 1L || port > 65535L) stop("Invalid PORT") +expected_origin <- required_env("APP_ORIGIN") +if (!identical(expected_origin, paste0("http://127.0.0.1:", port))) { + stop("APP_ORIGIN must match the fixed loopback listener origin.") +} +if (!identical(required_env("PGUSER"), "polls_app")) stop("Start with the runtime role.") +db <- new_pool() +onStop(function() pool::poolClose(db)) + +ui <- fluidPage( + tags$head(tags$link(rel = "stylesheet", href = "style.css")), + div(class = "page-shell", + div(class = "masthead", span(class = "brand", "Poll studio"), + span(class = "badge", "Synthetic workbench")), + div(class = "intro", p(class = "eyebrow", "A small decision, made visible"), + h1("Make room for a choice."), + p("Create a question, record sample responses and see the count take shape.")), + div(class = "workspace", + div(class = "panel", h2("Start a poll"), + textInput("question", "Question", placeholder = "What should our imaginary team try?"), + textAreaInput("choice_lines", "Choices, one per line", rows = 4, + placeholder = "Paper planets\nTiny gardens\nQuiet circuits"), + actionButton("create", "Create poll", class = "primary"), + p(class = "hint", "Two to eight distinct choices. No personal information.")), + div(class = "panel", h2("Record a response"), + selectInput("poll_id", "Stored poll", choices = character(), selectize = FALSE), + uiOutput("poll_state"), + radioButtons("choice_id", "Choose an option", choices = character(), selected = character()), + textInput("code", "Synthetic participant code", placeholder = "SAMPLE_001"), + actionButton("respond", "Save response", class = "primary"), + p(class = "hint", "Codes deduplicate responses; they do not identify people."), + verbatimTextOutput("last_result", placeholder = FALSE)), + div(class = "panel results", h2("The count so far"), + div(class = "toolbar", actionButton("refresh", "Refresh stored counts"), + actionButton("close", "Close selected poll")), + textOutput("summary_heading"), + plotOutput("counts", height = "260px"), + tableOutput("count_table"), + p(class = "hint", "Refresh to see another browser’s committed responses. Charts show counts only.")) + ), + div(class = "footnote", "Trusted local operators share authority. A matching saved response replays after closure; a different choice conflicts.") + ) +) + +server <- function(input, output, session) { + # Check the actual WebSocket request before registering database observers. + if (!trusted_origin(session$request, expected_origin)) { + session$close() + return(invisible(NULL)) + } + changed <- reactiveVal(0L) + last_result <- reactiveVal("") + pending_selection <- reactiveVal(NULL) + refresh <- function() changed(isolate(changed()) + 1L) + safely <- function(action) { + tryCatch(action(), poll_error = function(error) { + showNotification(conditionMessage(error), type = "warning") + }, error = function(error) { + showNotification("The database operation could not complete. Please retry.", type = "error") + }) + } + stored_polls <- reactive({ changed(); list_polls(db) }) + observe({ + rows <- stored_polls() + selected <- isolate(pending_selection()) + if (is.null(selected)) selected <- isolate(input$poll_id) + choices <- setNames(rows$id, paste0(rows$question, ifelse(rows$closed, " · closed", " · open"))) + if (length(selected) != 1L || is.na(selected) || !selected %in% rows$id) { + selected <- if (nrow(rows)) rows$id[[1]] else character() + } + updateSelectInput(session, "poll_id", choices = choices, selected = selected) + }) + summary <- reactive({ + changed() + req(isTruthy(input$poll_id)) + poll_summary(db, input$poll_id) + }) + observe({ + rows <- summary() + selected <- isolate(input$choice_id) + if (length(selected) != 1L || is.na(selected) || !selected %in% rows$choice_id) { + selected <- if (nrow(rows)) rows$choice_id[[1]] else character() + } + updateRadioButtons(session, "choice_id", choices = setNames(rows$choice_id, rows$label), selected = selected) + }) + output$poll_state <- renderUI({ + rows <- summary() + req(nrow(rows) > 0L) + div(class = if (rows$closed[[1]]) "state closed" else "state open", + if (rows$closed[[1]]) "Closed · saved matches can replay" else "Open · accepting sample responses") + }) + output$last_result <- renderText(last_result()) + output$summary_heading <- renderText({ + rows <- summary() + req(nrow(rows) > 0L) + paste(rows$question[[1]], "—", sum(rows$responses), "saved responses") + }) + output$counts <- renderPlot({ + rows <- summary() + req(nrow(rows) > 0L) + par(mar = c(4, 10, 1, 1), bg = "#ffffff") + labels <- ifelse(nchar(rows$label) > 18L, paste0(substr(rows$label, 1L, 17L), "…"), rows$label) + barplot(rows$responses, names.arg = labels, col = "#467763", border = NA, + horiz = TRUE, xlab = "Responses", xlim = c(0, max(1, rows$responses) + 1), + las = 1, cex.names = 0.8) + }) + output$count_table <- renderTable({ + rows <- summary() + data.frame(Choice = rows$label, Count = rows$responses) + }, rownames = FALSE, width = "100%") + observeEvent(input$create, safely(function() { + lines <- choice_lines(input$choice_lines) + id <- create_poll(db, input$question, lines) + pending_selection(id) + refresh() + updateTextInput(session, "question", value = "") + updateTextAreaInput(session, "choice_lines", value = "") + showNotification("Poll and choices saved.", type = "message") + })) + observeEvent(input$poll_id, { + if (identical(input$poll_id, isolate(pending_selection()))) pending_selection(NULL) + }) + observeEvent(input$respond, safely(function() { + result <- record_response(db, input$poll_id, input$choice_id, input$code) + last_result(if (result$replayed) "Matching saved response replayed." else "Response saved once.") + refresh() + })) + observeEvent(input$close, safely(function() { + close_poll(db, input$poll_id) + refresh() + showNotification("Poll closed. Saved matching responses remain repeatable.", type = "message") + })) + observeEvent(input$refresh, safely(refresh)) +} + +shinyApp(ui, server) diff --git a/applications/poll-studio/renv.lock b/applications/poll-studio/renv.lock new file mode 100644 index 00000000..dca0d5c2 --- /dev/null +++ b/applications/poll-studio/renv.lock @@ -0,0 +1,2112 @@ +{ + "R": { + "Version": "4.6.1", + "Repositories": [ + { + "Name": "CRAN", + "URL": "https://cloud.r-project.org" + } + ] + }, + "Packages": { + "DBI": { + "Package": "DBI", + "Version": "1.3.0", + "Source": "Repository", + "Title": "R Database Interface", + "Date": "2026-02-11", + "Authors@R": "c( person(\"R Special Interest Group on Databases (R-SIG-DB)\", role = \"aut\"), person(\"Hadley\", \"Wickham\", role = \"aut\"), person(\"Kirill\", \"Müller\", , \"kirill@cynkra.com\", role = c(\"aut\", \"cre\"), comment = c(ORCID = \"0000-0002-1416-3412\")), person(\"R Consortium\", role = \"fnd\") )", + "Description": "A database interface definition for communication between R and relational database management systems. All classes in this package are virtual and need to be extended by the various R/DBMS implementations.", + "License": "LGPL (>= 2.1)", + "URL": "https://dbi.r-dbi.org, https://github.com/r-dbi/DBI", + "BugReports": "https://github.com/r-dbi/DBI/issues", + "Depends": [ + "methods", + "R (>= 3.0.0)" + ], + "Suggests": [ + "arrow", + "blob", + "callr", + "covr", + "DBItest (>= 1.8.2)", + "dbplyr", + "downlit", + "dplyr", + "glue", + "hms", + "knitr", + "magrittr", + "nanoarrow (>= 0.3.0.1)", + "otel", + "otelsdk", + "RMariaDB", + "rmarkdown", + "rprojroot", + "RSQLite (>= 1.1-2)", + "testthat (>= 3.0.0)", + "vctrs", + "xml2" + ], + "VignetteBuilder": "knitr", + "Config/autostyle/scope": "line_breaks", + "Config/autostyle/strict": "false", + "Config/Needs/check": "r-dbi/DBItest", + "Config/Needs/website": "r-dbi/DBItest, r-dbi/dbitemplate, adbi, AzureKusto, bigrquery, DatabaseConnector, dittodb, duckdb, implyr, lazysf, odbc, pool, RAthena, IMSMWU/RClickhouse, RH2, RJDBC, RMariaDB, RMySQL, RPostgres, RPostgreSQL, RPresto, RSQLite, sergeant, sparklyr, withr", + "Config/testthat/edition": "3", + "Encoding": "UTF-8", + "RoxygenNote": "7.3.3.9000", + "NeedsCompilation": "no", + "Author": "R Special Interest Group on Databases (R-SIG-DB) [aut], Hadley Wickham [aut], Kirill Müller [aut, cre] (ORCID: ), R Consortium [fnd]", + "Maintainer": "Kirill Müller ", + "Repository": "CRAN" + }, + "KernSmooth": { + "Package": "KernSmooth", + "Version": "2.23-26", + "Source": "Repository", + "Priority": "recommended", + "Date": "2024-12-10", + "Title": "Functions for Kernel Smoothing Supporting Wand & Jones (1995)", + "Authors@R": "c(person(\"Matt\", \"Wand\", role = \"aut\", email = \"Matt.Wand@uts.edu.au\"), person(\"Cleve\", \"Moler\", role = \"ctb\", comment = \"LINPACK routines in src/d*\"), person(\"Brian\", \"Ripley\", role = c(\"trl\", \"cre\", \"ctb\"), email = \"Brian.Ripley@R-project.org\", comment = \"R port and updates\"))", + "Note": "Maintainers are not available to give advice on using a package they did not author.", + "Depends": [ + "R (>= 2.5.0)", + "stats" + ], + "Suggests": [ + "MASS", + "carData" + ], + "Description": "Functions for kernel smoothing (and density estimation) corresponding to the book: Wand, M.P. and Jones, M.C. (1995) \"Kernel Smoothing\".", + "License": "Unlimited", + "ByteCompile": "yes", + "NeedsCompilation": "yes", + "Author": "Matt Wand [aut], Cleve Moler [ctb] (LINPACK routines in src/d*), Brian Ripley [trl, cre, ctb] (R port and updates)", + "Maintainer": "Brian Ripley ", + "Repository": "CRAN" + }, + "MASS": { + "Package": "MASS", + "Version": "7.3-65", + "Source": "Repository", + "Priority": "recommended", + "Date": "2025-02-19", + "Revision": "$Rev: 3681 $", + "Depends": [ + "R (>= 4.4.0)", + "grDevices", + "graphics", + "stats", + "utils" + ], + "Imports": [ + "methods" + ], + "Suggests": [ + "lattice", + "nlme", + "nnet", + "survival" + ], + "Authors@R": "c(person(\"Brian\", \"Ripley\", role = c(\"aut\", \"cre\", \"cph\"), email = \"Brian.Ripley@R-project.org\"), person(\"Bill\", \"Venables\", role = c(\"aut\", \"cph\")), person(c(\"Douglas\", \"M.\"), \"Bates\", role = \"ctb\"), person(\"Kurt\", \"Hornik\", role = \"trl\", comment = \"partial port ca 1998\"), person(\"Albrecht\", \"Gebhardt\", role = \"trl\", comment = \"partial port ca 1998\"), person(\"David\", \"Firth\", role = \"ctb\", comment = \"support functions for polr\"))", + "Description": "Functions and datasets to support Venables and Ripley, \"Modern Applied Statistics with S\" (4th edition, 2002).", + "Title": "Support Functions and Datasets for Venables and Ripley's MASS", + "LazyData": "yes", + "ByteCompile": "yes", + "License": "GPL-2 | GPL-3", + "URL": "http://www.stats.ox.ac.uk/pub/MASS4/", + "Contact": "", + "NeedsCompilation": "yes", + "Author": "Brian Ripley [aut, cre, cph], Bill Venables [aut, cph], Douglas M. Bates [ctb], Kurt Hornik [trl] (partial port ca 1998), Albrecht Gebhardt [trl] (partial port ca 1998), David Firth [ctb] (support functions for polr)", + "Maintainer": "Brian Ripley ", + "Repository": "CRAN" + }, + "Matrix": { + "Package": "Matrix", + "Version": "1.7-5", + "Source": "Repository", + "VersionNote": "do also bump src/version.h, inst/include/Matrix/version.h", + "Date": "2026-03-20", + "Priority": "recommended", + "Title": "Sparse and Dense Matrix Classes and Methods", + "Description": "A rich hierarchy of sparse and dense matrix classes, including general, symmetric, triangular, and diagonal matrices with numeric, logical, or pattern entries. Efficient methods for operating on such matrices, often wrapping the 'BLAS', 'LAPACK', and 'SuiteSparse' libraries.", + "License": "GPL (>= 2) | file LICENCE", + "URL": "https://Matrix.R-forge.R-project.org", + "BugReports": "https://R-forge.R-project.org/tracker/?atid=294&group_id=61", + "Contact": "Matrix-authors@R-project.org", + "Authors@R": "c(person(\"Douglas\", \"Bates\", role = \"aut\", comment = c(ORCID = \"0000-0001-8316-9503\")), person(\"Martin\", \"Maechler\", role = c(\"aut\", \"cre\"), email = \"mmaechler+Matrix@gmail.com\", comment = c(ORCID = \"0000-0002-8685-9910\")), person(\"Mikael\", \"Jagan\", role = \"aut\", comment = c(ORCID = \"0000-0002-3542-2938\")), person(\"Timothy A.\", \"Davis\", role = \"ctb\", comment = c(ORCID = \"0000-0001-7614-6899\", \"SuiteSparse libraries\", \"collaborators listed in dir(system.file(\\\"doc\\\", \\\"SuiteSparse\\\", package=\\\"Matrix\\\"), pattern=\\\"License\\\", full.names=TRUE, recursive=TRUE)\")), person(\"George\", \"Karypis\", role = \"ctb\", comment = c(ORCID = \"0000-0003-2753-1437\", \"METIS library\", \"Copyright: Regents of the University of Minnesota\")), person(\"Jason\", \"Riedy\", role = \"ctb\", comment = c(ORCID = \"0000-0002-4345-4200\", \"GNU Octave's condest() and onenormest()\", \"Copyright: Regents of the University of California\")), person(\"Jens\", \"Oehlschlägel\", role = \"ctb\", comment = \"initial nearPD()\"), person(\"R Core Team\", role = \"ctb\", comment = c(ROR = \"02zz1nj61\", \"base R's matrix implementation\")))", + "Depends": [ + "R (>= 4.4)", + "methods" + ], + "Imports": [ + "grDevices", + "graphics", + "grid", + "lattice", + "stats", + "utils" + ], + "Suggests": [ + "MASS", + "datasets", + "sfsmisc", + "tools" + ], + "Enhances": [ + "SparseM", + "graph" + ], + "LazyData": "no", + "LazyDataNote": "not possible, since we use data/*.R and our S4 classes", + "BuildResaveData": "no", + "Encoding": "UTF-8", + "NeedsCompilation": "yes", + "Author": "Douglas Bates [aut] (ORCID: ), Martin Maechler [aut, cre] (ORCID: ), Mikael Jagan [aut] (ORCID: ), Timothy A. Davis [ctb] (ORCID: , SuiteSparse libraries, collaborators listed in dir(system.file(\"doc\", \"SuiteSparse\", package=\"Matrix\"), pattern=\"License\", full.names=TRUE, recursive=TRUE)), George Karypis [ctb] (ORCID: , METIS library, Copyright: Regents of the University of Minnesota), Jason Riedy [ctb] (ORCID: , GNU Octave's condest() and onenormest(), Copyright: Regents of the University of California), Jens Oehlschlägel [ctb] (initial nearPD()), R Core Team [ctb] (ROR: , base R's matrix implementation)", + "Maintainer": "Martin Maechler ", + "Repository": "CRAN" + }, + "R6": { + "Package": "R6", + "Version": "2.6.1", + "Source": "Repository", + "Title": "Encapsulated Classes with Reference Semantics", + "Authors@R": "c( person(\"Winston\", \"Chang\", , \"winston@posit.co\", role = c(\"aut\", \"cre\")), person(\"Posit Software, PBC\", role = c(\"cph\", \"fnd\")) )", + "Description": "Creates classes with reference semantics, similar to R's built-in reference classes. Compared to reference classes, R6 classes are simpler and lighter-weight, and they are not built on S4 classes so they do not require the methods package. These classes allow public and private members, and they support inheritance, even when the classes are defined in different packages.", + "License": "MIT + file LICENSE", + "URL": "https://r6.r-lib.org, https://github.com/r-lib/R6", + "BugReports": "https://github.com/r-lib/R6/issues", + "Depends": [ + "R (>= 3.6)" + ], + "Suggests": [ + "lobstr", + "testthat (>= 3.0.0)" + ], + "Config/Needs/website": "tidyverse/tidytemplate, ggplot2, microbenchmark, scales", + "Config/testthat/edition": "3", + "Encoding": "UTF-8", + "RoxygenNote": "7.3.2", + "NeedsCompilation": "no", + "Author": "Winston Chang [aut, cre], Posit Software, PBC [cph, fnd]", + "Maintainer": "Winston Chang ", + "Repository": "CRAN" + }, + "RPostgres": { + "Package": "RPostgres", + "Version": "1.4.10", + "Source": "Repository", + "Title": "C++ Interface to PostgreSQL", + "Date": "2026-02-11", + "Authors@R": "c( person(\"Hadley\", \"Wickham\", role = \"aut\"), person(\"Jeroen\", \"Ooms\", role = \"aut\"), person(\"Kirill\", \"Müller\", , \"kirill@cynkra.com\", role = c(\"aut\", \"cre\"), comment = c(ORCID = \"0000-0002-1416-3412\")), person(\"RStudio\", role = \"cph\"), person(\"R Consortium\", role = \"fnd\"), person(\"Tomoaki\", \"Nishiyama\", role = \"ctb\", comment = \"Code for encoding vectors into strings derived from RPostgreSQL\") )", + "Description": "Fully DBI-compliant C++-backed interface to PostgreSQL , an open-source relational database.", + "License": "MIT + file LICENSE", + "URL": "https://rpostgres.r-dbi.org, https://github.com/r-dbi/RPostgres", + "BugReports": "https://github.com/r-dbi/RPostgres/issues", + "Depends": [ + "R (>= 3.1.0)" + ], + "Imports": [ + "bit64", + "blob (>= 1.2.0)", + "DBI (>= 1.2.0)", + "hms (>= 1.0.0)", + "lubridate", + "methods", + "withr" + ], + "Suggests": [ + "callr", + "covr", + "DBItest (>= 1.7.3)", + "knitr", + "rlang", + "rmarkdown", + "testthat (>= 3.0.0)" + ], + "LinkingTo": [ + "cpp11" + ], + "Config/Needs/website": "r-dbi/dbitemplate", + "VignetteBuilder": "knitr", + "Config/Needs/build": "decor", + "Config/autostyle/scope": "line_breaks", + "Config/autostyle/strict": "false", + "Config/testthat/edition": "3", + "Encoding": "UTF-8", + "LazyLoad": "true", + "RoxygenNote": "7.3.3.9000", + "SystemRequirements": "libpq >= 9.0: libpq-dev (deb) or postgresql-devel (rpm)", + "Collate": "'PqDriver.R' 'PqConnection.R' 'PqResult.R' 'RPostgres-pkg.R' 'Redshift.R' 'cpp11.R' 'dbAppendTable_PqConnection.R' 'dbBegin_PqConnection.R' 'dbBind_PqResult.R' 'dbClearResult_PqResult.R' 'dbColumnInfo_PqResult.R' 'dbCommit_PqConnection.R' 'dbConnect_PqDriver.R' 'dbConnect_RedshiftDriver.R' 'dbDataType_PqConnection.R' 'dbDataType_PqDriver.R' 'dbDisconnect_PqConnection.R' 'dbExistsTable_PqConnection_Id.R' 'dbExistsTable_PqConnection_character.R' 'dbFetch_PqResult.R' 'dbGetInfo_PqConnection.R' 'dbGetInfo_PqDriver.R' 'dbGetRowCount_PqResult.R' 'dbGetRowsAffected_PqResult.R' 'dbGetStatement_PqResult.R' 'dbHasCompleted_PqResult.R' 'dbIsValid_PqConnection.R' 'dbIsValid_PqDriver.R' 'dbIsValid_PqResult.R' 'dbListFields_PqConnection_Id.R' 'dbListFields_PqConnection_character.R' 'dbListObjects_PqConnection_ANY.R' 'dbListTables_PqConnection.R' 'dbQuoteIdentifier_PqConnection_Id.R' 'dbQuoteIdentifier_PqConnection_SQL.R' 'dbQuoteIdentifier_PqConnection_character.R' 'dbQuoteLiteral_PqConnection.R' 'dbQuoteString_PqConnection_SQL.R' 'dbQuoteString_PqConnection_character.R' 'dbReadTable_PqConnection_character.R' 'dbRemoveTable_PqConnection_character.R' 'dbRollback_PqConnection.R' 'dbSendQuery_PqConnection.R' 'dbUnloadDriver_PqDriver.R' 'dbUnquoteIdentifier_PqConnection_SQL.R' 'dbWriteTable_PqConnection_character_data.frame.R' 'default.R' 'export.R' 'quote.R' 'show_PqConnection.R' 'sqlData_PqConnection.R' 'tables.R' 'transactions.R' 'utils.R'", + "NeedsCompilation": "yes", + "Author": "Hadley Wickham [aut], Jeroen Ooms [aut], Kirill Müller [aut, cre] (ORCID: ), RStudio [cph], R Consortium [fnd], Tomoaki Nishiyama [ctb] (Code for encoding vectors into strings derived from RPostgreSQL)", + "Maintainer": "Kirill Müller ", + "Repository": "CRAN" + }, + "Rcpp": { + "Package": "Rcpp", + "Version": "1.1.2", + "Source": "Repository", + "Title": "Seamless R and C++ Integration", + "Date": "2026-07-01", + "Authors@R": "c(person(\"Dirk\", \"Eddelbuettel\", role = c(\"aut\", \"cre\"), email = \"edd@debian.org\", comment = c(ORCID = \"0000-0001-6419-907X\")), person(\"Romain\", \"Francois\", role = \"aut\", comment = c(ORCID = \"0000-0002-2444-4226\")), person(\"JJ\", \"Allaire\", role = \"aut\", comment = c(ORCID = \"0000-0003-0174-9868\")), person(\"Kevin\", \"Ushey\", role = \"aut\", comment = c(ORCID = \"0000-0003-2880-7407\")), person(\"Qiang\", \"Kou\", role = \"aut\", comment = c(ORCID = \"0000-0001-6786-5453\")), person(\"Nathan\", \"Russell\", role = \"aut\"), person(\"Iñaki\", \"Ucar\", role = \"aut\", comment = c(ORCID = \"0000-0001-6403-5550\")), person(\"Doug\", \"Bates\", role = \"aut\", comment = c(ORCID = \"0000-0001-8316-9503\")), person(\"John\", \"Chambers\", role = \"aut\"))", + "Description": "The 'Rcpp' package provides R functions as well as C++ classes which offer a seamless integration of R and C++. Many R data types and objects can be mapped back and forth to C++ equivalents which facilitates both writing of new code as well as easier integration of third-party libraries. Documentation about 'Rcpp' is provided by several vignettes included in this package, via the 'Rcpp Gallery' site at , the paper by Eddelbuettel and Francois (2011, ), the book by Eddelbuettel (2013, ) and the paper by Eddelbuettel and Balamuta (2018, ); see 'citation(\"Rcpp\")' for details.", + "Depends": [ + "R (>= 3.5.0)" + ], + "Imports": [ + "methods", + "utils" + ], + "Suggests": [ + "tinytest", + "inline", + "rbenchmark", + "pkgKitten (>= 0.1.2)" + ], + "URL": "https://www.rcpp.org, https://dirk.eddelbuettel.com/code/rcpp.html, https://github.com/RcppCore/Rcpp", + "License": "GPL (>= 2)", + "BugReports": "https://github.com/RcppCore/Rcpp/issues", + "RoxygenNote": "6.1.1", + "Encoding": "UTF-8", + "VignetteBuilder": "Rcpp", + "NeedsCompilation": "yes", + "Author": "Dirk Eddelbuettel [aut, cre] (ORCID: ), Romain Francois [aut] (ORCID: ), JJ Allaire [aut] (ORCID: ), Kevin Ushey [aut] (ORCID: ), Qiang Kou [aut] (ORCID: ), Nathan Russell [aut], Iñaki Ucar [aut] (ORCID: ), Doug Bates [aut] (ORCID: ), John Chambers [aut]", + "Maintainer": "Dirk Eddelbuettel ", + "Repository": "CRAN" + }, + "base64enc": { + "Package": "base64enc", + "Version": "0.1-6", + "Source": "Repository", + "Title": "Tools for 'base64' Encoding", + "Author": "Simon Urbanek [aut, cre, cph] (https://urbanek.nz, ORCID: )", + "Authors@R": "person(\"Simon\", \"Urbanek\", role=c(\"aut\",\"cre\",\"cph\"), email=\"Simon.Urbanek@r-project.org\", comment=c(\"https://urbanek.nz\", ORCID=\"0000-0003-2297-1732\"))", + "Maintainer": "Simon Urbanek ", + "Depends": [ + "R (>= 2.9.0)" + ], + "Enhances": [ + "png" + ], + "Description": "Tools for handling 'base64' encoding. It is more flexible than the orphaned 'base64' package.", + "License": "GPL-2 | GPL-3", + "URL": "https://www.rforge.net/base64enc", + "BugReports": "https://github.com/s-u/base64enc/issues", + "NeedsCompilation": "yes", + "Repository": "CRAN" + }, + "bit": { + "Package": "bit", + "Version": "4.6.0", + "Source": "Repository", + "Title": "Classes and Methods for Fast Memory-Efficient Boolean Selections", + "Authors@R": "c( person(\"Michael\", \"Chirico\", email = \"MichaelChirico4@gmail.com\", role = c(\"aut\", \"cre\")), person(\"Jens\", \"Oehlschlägel\", role = \"aut\"), person(\"Brian\", \"Ripley\", role = \"ctb\") )", + "Depends": [ + "R (>= 3.4.0)" + ], + "Suggests": [ + "testthat (>= 3.0.0)", + "roxygen2", + "knitr", + "markdown", + "rmarkdown", + "microbenchmark", + "bit64 (>= 4.0.0)", + "ff (>= 4.0.0)" + ], + "Description": "Provided are classes for boolean and skewed boolean vectors, fast boolean methods, fast unique and non-unique integer sorting, fast set operations on sorted and unsorted sets of integers, and foundations for ff (range index, compression, chunked processing).", + "License": "GPL-2 | GPL-3", + "LazyLoad": "yes", + "ByteCompile": "yes", + "Encoding": "UTF-8", + "URL": "https://github.com/r-lib/bit", + "VignetteBuilder": "knitr, rmarkdown", + "RoxygenNote": "7.3.2", + "Config/testthat/edition": "3", + "NeedsCompilation": "yes", + "Author": "Michael Chirico [aut, cre], Jens Oehlschlägel [aut], Brian Ripley [ctb]", + "Maintainer": "Michael Chirico ", + "Repository": "CRAN" + }, + "bit64": { + "Package": "bit64", + "Version": "4.8.6", + "Source": "Repository", + "Title": "A S3 Class for Vectors of 64bit Integers", + "Authors@R": "c( person(\"Michael\", \"Chirico\", email=\"michaelchirico4@gmail.com\", role=c(\"aut\", \"cre\")), person(\"Jens\", \"Oehlschlägel\", role=\"aut\"), person(\"Leonardo\", \"Silvestri\", role=\"ctb\"), person(\"Ofek\", \"Shilon\", role=\"ctb\"), person(\"Christian\", \"Ullerich\", role=\"ctb\") )", + "Depends": [ + "R (>= 3.5.0)" + ], + "Description": "Package 'bit64' provides serializable S3 atomic 64bit (signed) integers. These are useful for handling database keys and exact counting in +-2^63. WARNING: do not use them as replacement for 32bit integers, integer64 are not supported for subscripting by R-core and they have different semantics when combined with double, e.g. integer64 + double => integer64. Class integer64 can be used in vectors, matrices, arrays and data.frames. Methods are available for coercion from and to logicals, integers, doubles, characters and factors as well as many elementwise and summary functions. Many fast algorithmic operations such as 'match' and 'order' support inter- active data exploration and manipulation and optionally leverage caching.", + "License": "GPL-2 | GPL-3", + "LazyLoad": "yes", + "ByteCompile": "yes", + "URL": "https://github.com/r-lib/bit64, https://bit64.r-lib.org", + "Encoding": "UTF-8", + "Imports": [ + "bit (>= 4.0.0)", + "graphics", + "methods", + "stats", + "utils" + ], + "Suggests": [ + "patrick (>= 0.3.0)", + "testthat (>= 3.3.0)", + "withr" + ], + "Config/testthat/edition": "3", + "Config/Needs/development": "patrick, testthat", + "Config/Needs/website": "tidyverse/tidytemplate", + "Config/roxygen2/version": "8.0.0", + "NeedsCompilation": "yes", + "Author": "Michael Chirico [aut, cre], Jens Oehlschlägel [aut], Leonardo Silvestri [ctb], Ofek Shilon [ctb], Christian Ullerich [ctb]", + "Maintainer": "Michael Chirico ", + "Repository": "CRAN" + }, + "blob": { + "Package": "blob", + "Version": "1.3.0", + "Source": "Repository", + "Title": "A Simple S3 Class for Representing Vectors of Binary Data ('BLOBS')", + "Authors@R": "c( person(\"Hadley\", \"Wickham\", role = \"aut\"), person(\"Kirill\", \"Müller\", , \"kirill@cynkra.com\", role = \"cre\"), person(\"RStudio\", role = c(\"cph\", \"fnd\")) )", + "Description": "R's raw vector is useful for storing a single binary object. What if you want to put a vector of them in a data frame? The 'blob' package provides the blob object, a list of raw vectors, suitable for use as a column in data frame.", + "License": "MIT + file LICENSE", + "URL": "https://blob.tidyverse.org, https://github.com/tidyverse/blob", + "BugReports": "https://github.com/tidyverse/blob/issues", + "Imports": [ + "methods", + "rlang", + "vctrs (>= 0.2.1)" + ], + "Suggests": [ + "covr", + "crayon", + "pillar (>= 1.2.1)", + "testthat (>= 3.0.0)" + ], + "Config/autostyle/scope": "line_breaks", + "Config/autostyle/strict": "false", + "Config/Needs/website": "tidyverse/tidytemplate", + "Config/testthat/edition": "3", + "Encoding": "UTF-8", + "RoxygenNote": "7.3.3.9000", + "NeedsCompilation": "no", + "Author": "Hadley Wickham [aut], Kirill Müller [cre], RStudio [cph, fnd]", + "Maintainer": "Kirill Müller ", + "Repository": "CRAN" + }, + "boot": { + "Package": "boot", + "Version": "1.3-32", + "Source": "Repository", + "Priority": "recommended", + "Date": "2025-08-29", + "Authors@R": "c(person(\"Angelo\", \"Canty\", role = \"aut\", email = \"cantya@mcmaster.ca\", comment = \"author of original code for S\"), person(\"Brian\", \"Ripley\", role = c(\"aut\", \"trl\"), email = \"Brian.Ripley@R-project.org\", comment = \"conversion to R, maintainer 1999--2022, author of parallel support\"), person(\"Alessandra R.\", \"Brazzale\", role = c(\"ctb\", \"cre\"), email = \"brazzale@stat.unipd.it\", comment = \"minor bug fixes\"))", + "Maintainer": "Alessandra R. Brazzale ", + "Note": "Maintainers are not available to give advice on using a package they did not author.", + "Description": "Functions and datasets for bootstrapping from the book \"Bootstrap Methods and Their Application\" by A. C. Davison and D. V. Hinkley (1997, CUP), originally written by Angelo Canty for S.", + "Title": "Bootstrap Functions", + "Depends": [ + "R (>= 3.0.0)", + "graphics", + "stats" + ], + "Suggests": [ + "MASS", + "survival" + ], + "LazyData": "yes", + "ByteCompile": "yes", + "License": "Unlimited", + "NeedsCompilation": "no", + "Author": "Angelo Canty [aut] (author of original code for S), Brian Ripley [aut, trl] (conversion to R, maintainer 1999--2022, author of parallel support), Alessandra R. Brazzale [ctb, cre] (minor bug fixes)", + "Repository": "CRAN" + }, + "bslib": { + "Package": "bslib", + "Version": "0.12.0", + "Source": "Repository", + "Title": "Custom 'Bootstrap' 'Sass' Themes for 'shiny' and 'rmarkdown'", + "Authors@R": "c( person(\"Carson\", \"Sievert\", , \"carson@posit.co\", role = c(\"aut\", \"cre\"), comment = c(ORCID = \"0000-0002-4958-2844\")), person(\"Joe\", \"Cheng\", , \"joe@posit.co\", role = \"aut\"), person(\"Garrick\", \"Aden-Buie\", , \"garrick@posit.co\", role = \"aut\", comment = c(ORCID = \"0000-0002-7111-0077\")), person(\"Posit Software, PBC\", role = c(\"cph\", \"fnd\")), person(, \"Bootstrap contributors\", role = \"ctb\", comment = \"Bootstrap library\"), person(, \"Twitter, Inc\", role = \"cph\", comment = \"Bootstrap library\"), person(\"Javi\", \"Aguilar\", role = c(\"ctb\", \"cph\"), comment = \"Bootstrap colorpicker library\"), person(\"Thomas\", \"Park\", role = c(\"ctb\", \"cph\"), comment = \"Bootswatch library\"), person(, \"PayPal\", role = c(\"ctb\", \"cph\"), comment = \"Bootstrap accessibility plugin\") )", + "Description": "Simplifies custom 'CSS' styling of both 'shiny' and 'rmarkdown' via 'Bootstrap' 'Sass'. Supports 'Bootstrap' 3, 4 and 5 as well as their various 'Bootswatch' themes. An interactive widget is also provided for previewing themes in real time.", + "License": "MIT + file LICENSE", + "URL": "https://rstudio.github.io/bslib/, https://github.com/rstudio/bslib", + "BugReports": "https://github.com/rstudio/bslib/issues", + "Depends": [ + "R (>= 2.10)" + ], + "Imports": [ + "base64enc", + "cachem", + "fastmap (>= 1.1.1)", + "grDevices", + "htmltools (>= 0.5.8)", + "jquerylib (>= 0.1.3)", + "jsonlite", + "lifecycle", + "memoise (>= 2.0.1)", + "mime", + "rlang", + "sass (>= 0.4.9)" + ], + "Suggests": [ + "brand.yml", + "bsicons", + "curl", + "fontawesome", + "future", + "ggplot2", + "knitr", + "lattice", + "magrittr", + "rappdirs", + "rmarkdown (>= 2.7)", + "shiny (>= 1.11.1.9000)", + "testthat", + "thematic", + "tools", + "utils", + "withr", + "yaml" + ], + "Config/Needs/deploy": "BH, chiflights22, colourpicker, commonmark, cpp11, cpsievert/chiflights22, cpsievert/histoslider, dplyr, DT, ggplot2, ggridges, gt, hexbin, histoslider, htmlwidgets, lattice, leaflet, lubridate, markdown, modelr, plotly, reactable, reshape2, rprojroot, rsconnect, rstudio/shiny, scales, styler, tibble", + "Config/Needs/routine": "chromote, desc, renv", + "Config/Needs/website": "brio, crosstalk, dplyr, DT, ggplot2, glue, htmlwidgets, leaflet, lorem, palmerpenguins, plotly, purrr, rprojroot, rstudio/htmltools, scales, stringr, tidyr, webshot2", + "Config/roxygen2/version": "8.0.0", + "Config/testthat/edition": "3", + "Config/testthat/parallel": "true", + "Config/testthat/start-first": "zzzz-bs-sass, fonts, zzz-precompile, theme-*, rmd-*", + "Encoding": "UTF-8", + "Collate": "'accordion.R' 'breakpoints.R' 'bs-current-theme.R' 'bs-dependencies.R' 'bs-global.R' 'bs-remove.R' 'bs-theme-layers.R' 'bs-theme-preset-bootswatch.R' 'bs-theme-preset-brand.R' 'bs-theme-preset-builtin.R' 'bs-theme-preset.R' 'utils.R' 'bs-theme-preview.R' 'bs-theme-update.R' 'bs-theme.R' 'bslib-package.R' 'buttons.R' 'card.R' 'deprecated.R' 'files.R' 'fill.R' 'imports.R' 'input-code-editor.R' 'input-dark-mode.R' 'input-submit.R' 'input-switch.R' 'layout.R' 'nav-items.R' 'nav-update.R' 'navbar_options.R' 'navs-legacy.R' 'navs.R' 'offcanvas.R' 'onLoad.R' 'page.R' 'popover.R' 'precompiled.R' 'print.R' 'shiny-devmode.R' 'sidebar.R' 'staticimports.R' 'toast.R' 'toolbar.R' 'tooltip.R' 'utils-deps.R' 'utils-shiny.R' 'utils-tags.R' 'value-box.R' 'version-default.R' 'versions.R'", + "NeedsCompilation": "no", + "Author": "Carson Sievert [aut, cre] (ORCID: ), Joe Cheng [aut], Garrick Aden-Buie [aut] (ORCID: ), Posit Software, PBC [cph, fnd], Bootstrap contributors [ctb] (Bootstrap library), Twitter, Inc [cph] (Bootstrap library), Javi Aguilar [ctb, cph] (Bootstrap colorpicker library), Thomas Park [ctb, cph] (Bootswatch library), PayPal [ctb, cph] (Bootstrap accessibility plugin)", + "Maintainer": "Carson Sievert ", + "Repository": "CRAN" + }, + "cachem": { + "Package": "cachem", + "Version": "1.1.0", + "Source": "Repository", + "Title": "Cache R Objects with Automatic Pruning", + "Description": "Key-value stores with automatic pruning. Caches can limit either their total size or the age of the oldest object (or both), automatically pruning objects to maintain the constraints.", + "Authors@R": "c( person(\"Winston\", \"Chang\", , \"winston@posit.co\", c(\"aut\", \"cre\")), person(family = \"Posit Software, PBC\", role = c(\"cph\", \"fnd\")))", + "License": "MIT + file LICENSE", + "Encoding": "UTF-8", + "ByteCompile": "true", + "URL": "https://cachem.r-lib.org/, https://github.com/r-lib/cachem", + "Imports": [ + "rlang", + "fastmap (>= 1.2.0)" + ], + "Suggests": [ + "testthat" + ], + "RoxygenNote": "7.2.3", + "Config/Needs/routine": "lobstr", + "Config/Needs/website": "pkgdown", + "NeedsCompilation": "yes", + "Author": "Winston Chang [aut, cre], Posit Software, PBC [cph, fnd]", + "Maintainer": "Winston Chang ", + "Repository": "CRAN" + }, + "class": { + "Package": "class", + "Version": "7.3-23", + "Source": "Repository", + "Priority": "recommended", + "Date": "2025-01-01", + "Depends": [ + "R (>= 3.0.0)", + "stats", + "utils" + ], + "Imports": [ + "MASS" + ], + "Authors@R": "c(person(\"Brian\", \"Ripley\", role = c(\"aut\", \"cre\", \"cph\"), email = \"Brian.Ripley@R-project.org\"), person(\"William\", \"Venables\", role = \"cph\"))", + "Description": "Various functions for classification, including k-nearest neighbour, Learning Vector Quantization and Self-Organizing Maps.", + "Title": "Functions for Classification", + "ByteCompile": "yes", + "License": "GPL-2 | GPL-3", + "URL": "http://www.stats.ox.ac.uk/pub/MASS4/", + "NeedsCompilation": "yes", + "Author": "Brian Ripley [aut, cre, cph], William Venables [cph]", + "Maintainer": "Brian Ripley ", + "Repository": "CRAN" + }, + "cli": { + "Package": "cli", + "Version": "3.6.6", + "Source": "Repository", + "Title": "Helpers for Developing Command Line Interfaces", + "Authors@R": "c( person(\"Gábor\", \"Csárdi\", , \"gabor@posit.co\", role = c(\"aut\", \"cre\")), person(\"Hadley\", \"Wickham\", role = \"ctb\"), person(\"Kirill\", \"Müller\", role = \"ctb\"), person(\"Salim\", \"Brüggemann\", , \"salim-b@pm.me\", role = \"ctb\", comment = c(ORCID = \"0000-0002-5329-5987\")), person(\"Posit Software, PBC\", role = c(\"cph\", \"fnd\"), comment = c(ROR = \"03wc8by49\")) )", + "Description": "A suite of tools to build attractive command line interfaces ('CLIs'), from semantic elements: headings, lists, alerts, paragraphs, etc. Supports custom themes via a 'CSS'-like language. It also contains a number of lower level 'CLI' elements: rules, boxes, trees, and 'Unicode' symbols with 'ASCII' alternatives. It support ANSI colors and text styles as well.", + "License": "MIT + file LICENSE", + "URL": "https://cli.r-lib.org, https://github.com/r-lib/cli", + "BugReports": "https://github.com/r-lib/cli/issues", + "Depends": [ + "R (>= 3.4)" + ], + "Imports": [ + "utils" + ], + "Suggests": [ + "callr", + "covr", + "crayon", + "digest", + "glue (>= 1.6.0)", + "grDevices", + "htmltools", + "htmlwidgets", + "knitr", + "methods", + "processx", + "ps (>= 1.3.4.9000)", + "rlang (>= 1.0.2.9003)", + "rmarkdown", + "rprojroot", + "rstudioapi", + "testthat (>= 3.2.0)", + "tibble", + "whoami", + "withr" + ], + "Config/Needs/website": "r-lib/asciicast, bench, brio, cpp11, decor, desc, fansi, prettyunits, sessioninfo, tidyverse/tidytemplate, usethis, vctrs", + "Config/testthat/edition": "3", + "Config/usethis/last-upkeep": "2025-04-25", + "Encoding": "UTF-8", + "RoxygenNote": "7.3.2.9000", + "NeedsCompilation": "yes", + "Author": "Gábor Csárdi [aut, cre], Hadley Wickham [ctb], Kirill Müller [ctb], Salim Brüggemann [ctb] (ORCID: ), Posit Software, PBC [cph, fnd] (ROR: )", + "Maintainer": "Gábor Csárdi ", + "Repository": "CRAN" + }, + "cluster": { + "Package": "cluster", + "Version": "2.1.8.2", + "Source": "Repository", + "VersionNote": "Last CRAN: 2.1.8.1 on 2025-03-11; 2.1.8 on 2024-12-10; 2.1.7 on 2024-12-06; 2.1.6 on 2023-11-30;", + "Date": "2026-02-03", + "Priority": "recommended", + "Title": "\"Finding Groups in Data\": Cluster Analysis Extended Rousseeuw et al.", + "Description": "Methods for Cluster analysis. Much extended the original from Peter Rousseeuw, Anja Struyf and Mia Hubert, based on Kaufman and Rousseeuw (1990) \"Finding Groups in Data\".", + "Maintainer": "Martin Maechler ", + "Authors@R": "c(person(\"Martin\",\"Maechler\", role = c(\"aut\",\"cre\"), email=\"maechler@stat.math.ethz.ch\", comment = c(ORCID = \"0000-0002-8685-9910\")) ,person(\"Peter\", \"Rousseeuw\", role=\"aut\", email=\"peter.rousseeuw@kuleuven.be\", comment = c(\"Fortran original\", ORCID = \"0000-0002-3807-5353\")) ,person(\"Anja\", \"Struyf\", role=\"aut\", comment= \"S original\") ,person(\"Mia\", \"Hubert\", role=\"aut\", email= \"Mia.Hubert@uia.ua.ac.be\", comment = c(\"S original\", ORCID = \"0000-0001-6398-4850\")) ,person(\"Kurt\", \"Hornik\", role=c(\"trl\", \"ctb\"), email=\"Kurt.Hornik@R-project.org\", comment=c(\"port to R; maintenance(1999-2000)\", ORCID=\"0000-0003-4198-9911\")) ,person(\"Matthias\", \"Studer\", role=\"ctb\") ,person(\"Pierre\", \"Roudier\", role=\"ctb\") ,person(\"Juan\", \"Gonzalez\", role=\"ctb\") ,person(\"Kamil\", \"Kozlowski\", role=\"ctb\") ,person(\"Erich\", \"Schubert\", role=\"ctb\", comment = c(\"fastpam options for pam()\", ORCID = \"0000-0001-9143-4880\")) ,person(\"Keefe\", \"Murphy\", role=\"ctb\", comment = \"volume.ellipsoid({d >= 3})\") #not yet ,person(\"Fischer-Rasmussen\", \"Kasper\", role = \"ctb\", comment = \"Gower distance for CLARA\") )", + "Depends": [ + "R (>= 3.5.0)" + ], + "Imports": [ + "graphics", + "grDevices", + "stats", + "utils" + ], + "Suggests": [ + "MASS", + "Matrix" + ], + "SuggestsNote": "MASS: two examples using cov.rob() and mvrnorm(); Matrix tools for testing", + "Enhances": [ + "mvoutlier", + "fpc", + "ellipse", + "sfsmisc" + ], + "EnhancesNote": "xref-ed in man/*.Rd", + "LazyLoad": "yes", + "LazyData": "yes", + "ByteCompile": "yes", + "BuildResaveData": "no", + "License": "GPL (>= 2)", + "URL": "https://svn.r-project.org/R-packages/trunk/cluster/", + "NeedsCompilation": "yes", + "Author": "Martin Maechler [aut, cre] (ORCID: ), Peter Rousseeuw [aut] (Fortran original, ORCID: ), Anja Struyf [aut] (S original), Mia Hubert [aut] (S original, ORCID: ), Kurt Hornik [trl, ctb] (port to R; maintenance(1999-2000), ORCID: ), Matthias Studer [ctb], Pierre Roudier [ctb], Juan Gonzalez [ctb], Kamil Kozlowski [ctb], Erich Schubert [ctb] (fastpam options for pam(), ORCID: ), Keefe Murphy [ctb] (volume.ellipsoid({d >= 3}))", + "Repository": "CRAN" + }, + "codetools": { + "Package": "codetools", + "Version": "0.2-20", + "Source": "Repository", + "Priority": "recommended", + "Author": "Luke Tierney ", + "Description": "Code analysis tools for R.", + "Title": "Code Analysis Tools for R", + "Depends": [ + "R (>= 2.1)" + ], + "Maintainer": "Luke Tierney ", + "URL": "https://gitlab.com/luke-tierney/codetools", + "License": "GPL", + "NeedsCompilation": "no", + "Repository": "CRAN" + }, + "commonmark": { + "Package": "commonmark", + "Version": "2.0.0", + "Source": "Repository", + "Type": "Package", + "Title": "High Performance CommonMark and Github Markdown Rendering in R", + "Authors@R": "c( person(\"Jeroen\", \"Ooms\", ,\"jeroenooms@gmail.com\", role = c(\"aut\", \"cre\"), comment = c(ORCID = \"0000-0002-4035-0289\")), person(\"John MacFarlane\", role = \"cph\", comment = \"Author of cmark\"))", + "Description": "The CommonMark specification defines a rationalized version of markdown syntax. This package uses the 'cmark' reference implementation for converting markdown text into various formats including html, latex and groff man. In addition it exposes the markdown parse tree in xml format. Also includes opt-in support for GFM extensions including tables, autolinks, and strikethrough text.", + "License": "BSD_2_clause + file LICENSE", + "URL": "https://docs.ropensci.org/commonmark/ https://ropensci.r-universe.dev/commonmark", + "BugReports": "https://github.com/r-lib/commonmark/issues", + "Suggests": [ + "curl", + "testthat", + "xml2" + ], + "RoxygenNote": "7.3.2", + "Language": "en-US", + "Encoding": "UTF-8", + "NeedsCompilation": "yes", + "Author": "Jeroen Ooms [aut, cre] (ORCID: ), John MacFarlane [cph] (Author of cmark)", + "Maintainer": "Jeroen Ooms ", + "Repository": "CRAN" + }, + "cpp11": { + "Package": "cpp11", + "Version": "0.5.5", + "Source": "Repository", + "Title": "A C++11 Interface for R's C Interface", + "Authors@R": "c( person(\"Davis\", \"Vaughan\", email = \"davis@posit.co\", role = c(\"aut\", \"cre\"), comment = c(ORCID = \"0000-0003-4777-038X\")), person(\"Jim\",\"Hester\", role = \"aut\", comment = c(ORCID = \"0000-0002-2739-7082\")), person(\"Romain\", \"François\", role = \"aut\", comment = c(ORCID = \"0000-0002-2444-4226\")), person(\"Benjamin\", \"Kietzman\", role = \"ctb\"), person(\"Posit Software, PBC\", role = c(\"cph\", \"fnd\")) )", + "Description": "Provides a header only, C++11 interface to R's C interface. Compared to other approaches 'cpp11' strives to be safe against long jumps from the C API as well as C++ exceptions, conform to normal R function semantics and supports interaction with 'ALTREP' vectors.", + "License": "MIT + file LICENSE", + "URL": "https://cpp11.r-lib.org, https://github.com/r-lib/cpp11", + "BugReports": "https://github.com/r-lib/cpp11/issues", + "Depends": [ + "R (>= 4.0.0)" + ], + "Suggests": [ + "bench", + "brio", + "callr", + "cli", + "covr", + "decor", + "desc", + "ggplot2", + "glue", + "knitr", + "lobstr", + "mockery", + "progress", + "rmarkdown", + "scales", + "Rcpp", + "testthat (>= 3.2.0)", + "tibble", + "utils", + "vctrs", + "withr" + ], + "VignetteBuilder": "knitr", + "Config/Needs/website": "tidyverse/tidytemplate", + "Config/testthat/edition": "3", + "Config/Needs/cpp11/cpp_register": "brio, cli, decor, desc, glue, tibble, vctrs", + "Encoding": "UTF-8", + "RoxygenNote": "7.3.3", + "NeedsCompilation": "no", + "Author": "Davis Vaughan [aut, cre] (ORCID: ), Jim Hester [aut] (ORCID: ), Romain François [aut] (ORCID: ), Benjamin Kietzman [ctb], Posit Software, PBC [cph, fnd]", + "Maintainer": "Davis Vaughan ", + "Repository": "CRAN" + }, + "digest": { + "Package": "digest", + "Version": "0.6.39", + "Source": "Repository", + "Authors@R": "c(person(\"Dirk\", \"Eddelbuettel\", role = c(\"aut\", \"cre\"), email = \"edd@debian.org\", comment = c(ORCID = \"0000-0001-6419-907X\")), person(\"Antoine\", \"Lucas\", role=\"ctb\", comment = c(ORCID = \"0000-0002-8059-9767\")), person(\"Jarek\", \"Tuszynski\", role=\"ctb\"), person(\"Henrik\", \"Bengtsson\", role=\"ctb\", comment = c(ORCID = \"0000-0002-7579-5165\")), person(\"Simon\", \"Urbanek\", role=\"ctb\", comment = c(ORCID = \"0000-0003-2297-1732\")), person(\"Mario\", \"Frasca\", role=\"ctb\"), person(\"Bryan\", \"Lewis\", role=\"ctb\"), person(\"Murray\", \"Stokely\", role=\"ctb\"), person(\"Hannes\", \"Muehleisen\", role=\"ctb\", comment = c(ORCID = \"0000-0001-8552-0029\")), person(\"Duncan\", \"Murdoch\", role=\"ctb\"), person(\"Jim\", \"Hester\", role=\"ctb\", comment = c(ORCID = \"0000-0002-2739-7082\")), person(\"Wush\", \"Wu\", role=\"ctb\", comment = c(ORCID = \"0000-0001-5180-0567\")), person(\"Qiang\", \"Kou\", role=\"ctb\", comment = c(ORCID = \"0000-0001-6786-5453\")), person(\"Thierry\", \"Onkelinx\", role=\"ctb\", comment = c(ORCID = \"0000-0001-8804-4216\")), person(\"Michel\", \"Lang\", role=\"ctb\", comment = c(ORCID = \"0000-0001-9754-0393\")), person(\"Viliam\", \"Simko\", role=\"ctb\"), person(\"Kurt\", \"Hornik\", role=\"ctb\", comment = c(ORCID = \"0000-0003-4198-9911\")), person(\"Radford\", \"Neal\", role=\"ctb\", comment = c(ORCID = \"0000-0002-2473-3407\")), person(\"Kendon\", \"Bell\", role=\"ctb\", comment = c(ORCID = \"0000-0002-9093-8312\")), person(\"Matthew\", \"de Queljoe\", role=\"ctb\"), person(\"Dmitry\", \"Selivanov\", role=\"ctb\", comment = c(ORCID = \"0000-0003-0492-6647\")), person(\"Ion\", \"Suruceanu\", role=\"ctb\", comment = c(ORCID = \"0009-0005-6446-4909\")), person(\"Bill\", \"Denney\", role=\"ctb\", comment = c(ORCID = \"0000-0002-5759-428X\")), person(\"Dirk\", \"Schumacher\", role=\"ctb\"), person(\"András\", \"Svraka\", role=\"ctb\", comment = c(ORCID = \"0009-0008-8480-1329\")), person(\"Sergey\", \"Fedorov\", role=\"ctb\", comment = c(ORCID = \"0000-0002-5970-7233\")), person(\"Will\", \"Landau\", role=\"ctb\", comment = c(ORCID = \"0000-0003-1878-3253\")), person(\"Floris\", \"Vanderhaeghe\", role=\"ctb\", comment = c(ORCID = \"0000-0002-6378-6229\")), person(\"Kevin\", \"Tappe\", role=\"ctb\"), person(\"Harris\", \"McGehee\", role=\"ctb\"), person(\"Tim\", \"Mastny\", role=\"ctb\"), person(\"Aaron\", \"Peikert\", role=\"ctb\", comment = c(ORCID = \"0000-0001-7813-818X\")), person(\"Mark\", \"van der Loo\", role=\"ctb\", comment = c(ORCID = \"0000-0002-9807-4686\")), person(\"Chris\", \"Muir\", role=\"ctb\", comment = c(ORCID = \"0000-0003-2555-3878\")), person(\"Moritz\", \"Beller\", role=\"ctb\", comment = c(ORCID = \"0000-0003-4852-0526\")), person(\"Sebastian\", \"Campbell\", role=\"ctb\", comment = c(ORCID = \"0009-0000-5948-4503\")), person(\"Winston\", \"Chang\", role=\"ctb\", comment = c(ORCID = \"0000-0002-1576-2126\")), person(\"Dean\", \"Attali\", role=\"ctb\", comment = c(ORCID = \"0000-0002-5645-3493\")), person(\"Michael\", \"Chirico\", role=\"ctb\", comment = c(ORCID = \"0000-0003-0787-087X\")), person(\"Kevin\", \"Ushey\", role=\"ctb\", comment = c(ORCID = \"0000-0003-2880-7407\")), person(\"Carl\", \"Pearson\", role=\"ctb\", comment = c(ORCID = \"0000-0003-0701-7860\")))", + "Date": "2025-11-19", + "Title": "Create Compact Hash Digests of R Objects", + "Description": "Implementation of a function 'digest()' for the creation of hash digests of arbitrary R objects (using the 'md5', 'sha-1', 'sha-256', 'crc32', 'xxhash', 'murmurhash', 'spookyhash', 'blake3', 'crc32c', 'xxh3_64', and 'xxh3_128' algorithms) permitting easy comparison of R language objects, as well as functions such as 'hmac()' to create hash-based message authentication code. Please note that this package is not meant to be deployed for cryptographic purposes for which more comprehensive (and widely tested) libraries such as 'OpenSSL' should be used.", + "URL": "https://github.com/eddelbuettel/digest, https://eddelbuettel.github.io/digest/, https://dirk.eddelbuettel.com/code/digest.html", + "BugReports": "https://github.com/eddelbuettel/digest/issues", + "Depends": [ + "R (>= 3.3.0)" + ], + "Imports": [ + "utils" + ], + "License": "GPL (>= 2)", + "Suggests": [ + "tinytest", + "simplermarkdown", + "rbenchmark" + ], + "VignetteBuilder": "simplermarkdown", + "Encoding": "UTF-8", + "NeedsCompilation": "yes", + "Author": "Dirk Eddelbuettel [aut, cre] (ORCID: ), Antoine Lucas [ctb] (ORCID: ), Jarek Tuszynski [ctb], Henrik Bengtsson [ctb] (ORCID: ), Simon Urbanek [ctb] (ORCID: ), Mario Frasca [ctb], Bryan Lewis [ctb], Murray Stokely [ctb], Hannes Muehleisen [ctb] (ORCID: ), Duncan Murdoch [ctb], Jim Hester [ctb] (ORCID: ), Wush Wu [ctb] (ORCID: ), Qiang Kou [ctb] (ORCID: ), Thierry Onkelinx [ctb] (ORCID: ), Michel Lang [ctb] (ORCID: ), Viliam Simko [ctb], Kurt Hornik [ctb] (ORCID: ), Radford Neal [ctb] (ORCID: ), Kendon Bell [ctb] (ORCID: ), Matthew de Queljoe [ctb], Dmitry Selivanov [ctb] (ORCID: ), Ion Suruceanu [ctb] (ORCID: ), Bill Denney [ctb] (ORCID: ), Dirk Schumacher [ctb], András Svraka [ctb] (ORCID: ), Sergey Fedorov [ctb] (ORCID: ), Will Landau [ctb] (ORCID: ), Floris Vanderhaeghe [ctb] (ORCID: ), Kevin Tappe [ctb], Harris McGehee [ctb], Tim Mastny [ctb], Aaron Peikert [ctb] (ORCID: ), Mark van der Loo [ctb] (ORCID: ), Chris Muir [ctb] (ORCID: ), Moritz Beller [ctb] (ORCID: ), Sebastian Campbell [ctb] (ORCID: ), Winston Chang [ctb] (ORCID: ), Dean Attali [ctb] (ORCID: ), Michael Chirico [ctb] (ORCID: ), Kevin Ushey [ctb] (ORCID: ), Carl Pearson [ctb] (ORCID: )", + "Maintainer": "Dirk Eddelbuettel ", + "Repository": "CRAN" + }, + "fastmap": { + "Package": "fastmap", + "Version": "1.2.0", + "Source": "Repository", + "Title": "Fast Data Structures", + "Authors@R": "c( person(\"Winston\", \"Chang\", email = \"winston@posit.co\", role = c(\"aut\", \"cre\")), person(given = \"Posit Software, PBC\", role = c(\"cph\", \"fnd\")), person(given = \"Tessil\", role = \"cph\", comment = \"hopscotch_map library\") )", + "Description": "Fast implementation of data structures, including a key-value store, stack, and queue. Environments are commonly used as key-value stores in R, but every time a new key is used, it is added to R's global symbol table, causing a small amount of memory leakage. This can be problematic in cases where many different keys are used. Fastmap avoids this memory leak issue by implementing the map using data structures in C++.", + "License": "MIT + file LICENSE", + "Encoding": "UTF-8", + "RoxygenNote": "7.2.3", + "Suggests": [ + "testthat (>= 2.1.1)" + ], + "URL": "https://r-lib.github.io/fastmap/, https://github.com/r-lib/fastmap", + "BugReports": "https://github.com/r-lib/fastmap/issues", + "NeedsCompilation": "yes", + "Author": "Winston Chang [aut, cre], Posit Software, PBC [cph, fnd], Tessil [cph] (hopscotch_map library)", + "Maintainer": "Winston Chang ", + "Repository": "CRAN" + }, + "fontawesome": { + "Package": "fontawesome", + "Version": "0.5.3", + "Source": "Repository", + "Type": "Package", + "Title": "Easily Work with 'Font Awesome' Icons", + "Description": "Easily and flexibly insert 'Font Awesome' icons into 'R Markdown' documents and 'Shiny' apps. These icons can be inserted into HTML content through inline 'SVG' tags or 'i' tags. There is also a utility function for exporting 'Font Awesome' icons as 'PNG' images for those situations where raster graphics are needed.", + "Authors@R": "c( person(\"Richard\", \"Iannone\", , \"rich@posit.co\", c(\"aut\", \"cre\"), comment = c(ORCID = \"0000-0003-3925-190X\")), person(\"Christophe\", \"Dervieux\", , \"cderv@posit.co\", role = \"ctb\", comment = c(ORCID = \"0000-0003-4474-2498\")), person(\"Winston\", \"Chang\", , \"winston@posit.co\", role = \"ctb\"), person(\"Dave\", \"Gandy\", role = c(\"ctb\", \"cph\"), comment = \"Font-Awesome font\"), person(\"Posit Software, PBC\", role = c(\"cph\", \"fnd\")) )", + "License": "MIT + file LICENSE", + "URL": "https://github.com/rstudio/fontawesome, https://rstudio.github.io/fontawesome/", + "BugReports": "https://github.com/rstudio/fontawesome/issues", + "Encoding": "UTF-8", + "ByteCompile": "true", + "RoxygenNote": "7.3.2", + "Depends": [ + "R (>= 3.3.0)" + ], + "Imports": [ + "rlang (>= 1.0.6)", + "htmltools (>= 0.5.1.1)" + ], + "Suggests": [ + "covr", + "dplyr (>= 1.0.8)", + "gt (>= 0.9.0)", + "knitr (>= 1.31)", + "testthat (>= 3.0.0)", + "rsvg" + ], + "Config/testthat/edition": "3", + "NeedsCompilation": "no", + "Author": "Richard Iannone [aut, cre] (), Christophe Dervieux [ctb] (), Winston Chang [ctb], Dave Gandy [ctb, cph] (Font-Awesome font), Posit Software, PBC [cph, fnd]", + "Maintainer": "Richard Iannone ", + "Repository": "CRAN" + }, + "foreign": { + "Package": "foreign", + "Version": "0.8-91", + "Source": "Repository", + "Priority": "recommended", + "Date": "2026-01-29", + "Title": "Read Data Stored by 'Minitab', 'S', 'SAS', 'SPSS', 'Stata', 'Systat', 'Weka', 'dBase', ...", + "Depends": [ + "R (>= 4.0.0)" + ], + "Imports": [ + "methods", + "utils", + "stats" + ], + "Authors@R": "c( person(\"R Core Team\", email = \"R-core@R-project.org\", role = c(\"aut\", \"cph\", \"cre\"), comment = c(ROR = \"02zz1nj61\")), person(\"Roger\", \"Bivand\", role = c(\"ctb\", \"cph\")), person(c(\"Vincent\", \"J.\"), \"Carey\", role = c(\"ctb\", \"cph\")), person(\"Saikat\", \"DebRoy\", role = c(\"ctb\", \"cph\")), person(\"Stephen\", \"Eglen\", role = c(\"ctb\", \"cph\")), person(\"Rajarshi\", \"Guha\", role = c(\"ctb\", \"cph\")), person(\"Swetlana\", \"Herbrandt\", role = \"ctb\"), person(\"Nicholas\", \"Lewin-Koh\", role = c(\"ctb\", \"cph\")), person(\"Mark\", \"Myatt\", role = c(\"ctb\", \"cph\")), person(\"Michael\", \"Nelson\", role = \"ctb\"), person(\"Ben\", \"Pfaff\", role = \"ctb\"), person(\"Brian\", \"Quistorff\", role = \"ctb\"), person(\"Frank\", \"Warmerdam\", role = c(\"ctb\", \"cph\")), person(\"Stephen\", \"Weigand\", role = c(\"ctb\", \"cph\")), person(\"Free Software Foundation, Inc.\", role = \"cph\"))", + "Contact": "see 'MailingList'", + "Copyright": "see file COPYRIGHTS", + "Description": "Reading and writing data stored by some versions of 'Epi Info', 'Minitab', 'S', 'SAS', 'SPSS', 'Stata', 'Systat', 'Weka', and for reading and writing some 'dBase' files.", + "ByteCompile": "yes", + "Biarch": "yes", + "License": "GPL (>= 2)", + "BugReports": "https://bugs.r-project.org", + "MailingList": "R-help@r-project.org", + "URL": "https://svn.r-project.org/R-packages/trunk/foreign/", + "NeedsCompilation": "yes", + "Author": "R Core Team [aut, cph, cre] (ROR: ), Roger Bivand [ctb, cph], Vincent J. Carey [ctb, cph], Saikat DebRoy [ctb, cph], Stephen Eglen [ctb, cph], Rajarshi Guha [ctb, cph], Swetlana Herbrandt [ctb], Nicholas Lewin-Koh [ctb, cph], Mark Myatt [ctb, cph], Michael Nelson [ctb], Ben Pfaff [ctb], Brian Quistorff [ctb], Frank Warmerdam [ctb, cph], Stephen Weigand [ctb, cph], Free Software Foundation, Inc. [cph]", + "Maintainer": "R Core Team ", + "Repository": "CRAN" + }, + "fs": { + "Package": "fs", + "Version": "2.1.0", + "Source": "Repository", + "Title": "Cross-Platform File System Operations Based on 'libuv'", + "Authors@R": "c( person(\"Jim\", \"Hester\", role = \"aut\"), person(\"Hadley\", \"Wickham\", role = \"aut\"), person(\"Gábor\", \"Csárdi\", role = \"aut\"), person(\"Jeroen\", \"Ooms\", , \"jeroenooms@gmail.com\", role = \"cre\"), person(\"libuv project contributors\", role = \"cph\", comment = \"libuv library\"), person(\"Joyent, Inc. and other Node contributors\", role = \"cph\", comment = \"libuv library\"), person(\"Posit Software, PBC\", role = c(\"cph\", \"fnd\"), comment = c(ROR = \"03wc8by49\")) )", + "Description": "A cross-platform interface to file system operations, built on top of the 'libuv' C library.", + "License": "MIT + file LICENSE", + "URL": "https://fs.r-lib.org, https://github.com/r-lib/fs", + "BugReports": "https://github.com/r-lib/fs/issues", + "Depends": [ + "R (>= 4.1)" + ], + "Imports": [ + "methods" + ], + "Suggests": [ + "covr", + "crayon", + "knitr", + "pillar (>= 1.0.0)", + "rmarkdown", + "spelling", + "testthat (>= 3.0.0)", + "tibble (>= 1.1.0)", + "vctrs (>= 0.3.0)", + "withr" + ], + "VignetteBuilder": "knitr", + "SystemRequirements": "libuv: libuv-devel (rpm) or libuv1-dev (deb). Alternatively to build the vendored libuv 'cmake' is required. GNU make.", + "Config/Needs/website": "tidyverse/tidytemplate", + "Config/testthat/edition": "3", + "Config/usethis/last-upkeep": "2025-04-23", + "Copyright": "file COPYRIGHTS", + "Encoding": "UTF-8", + "Language": "en-US", + "RoxygenNote": "7.3.3", + "NeedsCompilation": "yes", + "Author": "Jim Hester [aut], Hadley Wickham [aut], Gábor Csárdi [aut], Jeroen Ooms [cre], libuv project contributors [cph] (libuv library), Joyent, Inc. and other Node contributors [cph] (libuv library), Posit Software, PBC [cph, fnd] (ROR: )", + "Maintainer": "Jeroen Ooms ", + "Repository": "CRAN" + }, + "generics": { + "Package": "generics", + "Version": "0.1.4", + "Source": "Repository", + "Title": "Common S3 Generics not Provided by Base R Methods Related to Model Fitting", + "Authors@R": "c( person(\"Hadley\", \"Wickham\", , \"hadley@posit.co\", role = c(\"aut\", \"cre\"), comment = c(ORCID = \"0000-0003-4757-117X\")), person(\"Max\", \"Kuhn\", , \"max@posit.co\", role = \"aut\"), person(\"Davis\", \"Vaughan\", , \"davis@posit.co\", role = \"aut\"), person(\"Posit Software, PBC\", role = c(\"cph\", \"fnd\"), comment = c(ROR = \"https://ror.org/03wc8by49\")) )", + "Description": "In order to reduce potential package dependencies and conflicts, generics provides a number of commonly used S3 generics.", + "License": "MIT + file LICENSE", + "URL": "https://generics.r-lib.org, https://github.com/r-lib/generics", + "BugReports": "https://github.com/r-lib/generics/issues", + "Depends": [ + "R (>= 3.6)" + ], + "Imports": [ + "methods" + ], + "Suggests": [ + "covr", + "pkgload", + "testthat (>= 3.0.0)", + "tibble", + "withr" + ], + "Config/Needs/website": "tidyverse/tidytemplate", + "Config/testthat/edition": "3", + "Encoding": "UTF-8", + "RoxygenNote": "7.3.2", + "NeedsCompilation": "no", + "Author": "Hadley Wickham [aut, cre] (ORCID: ), Max Kuhn [aut], Davis Vaughan [aut], Posit Software, PBC [cph, fnd] (ROR: )", + "Maintainer": "Hadley Wickham ", + "Repository": "CRAN" + }, + "glue": { + "Package": "glue", + "Version": "1.8.1", + "Source": "Repository", + "Title": "Interpreted String Literals", + "Authors@R": "c( person(\"Jim\", \"Hester\", role = \"aut\", comment = c(ORCID = \"0000-0002-2739-7082\")), person(\"Jennifer\", \"Bryan\", , \"jenny@posit.co\", role = c(\"aut\", \"cre\"), comment = c(ORCID = \"0000-0002-6983-2759\")), person(\"Posit Software, PBC\", role = c(\"cph\", \"fnd\"), comment = c(ROR = \"03wc8by49\")) )", + "Description": "An implementation of interpreted string literals, inspired by Python's Literal String Interpolation and Docstrings and Julia's Triple-Quoted String Literals .", + "License": "MIT + file LICENSE", + "URL": "https://glue.tidyverse.org/, https://github.com/tidyverse/glue", + "BugReports": "https://github.com/tidyverse/glue/issues", + "Depends": [ + "R (>= 4.1)" + ], + "Imports": [ + "methods" + ], + "Suggests": [ + "crayon", + "DBI (>= 1.2.0)", + "dplyr", + "knitr", + "rlang", + "rmarkdown", + "RSQLite", + "testthat (>= 3.2.0)", + "vctrs (>= 0.3.0)", + "waldo (>= 0.5.3)", + "withr" + ], + "VignetteBuilder": "knitr", + "ByteCompile": "true", + "Config/Needs/website": "bench, forcats, ggbeeswarm, ggplot2, R.utils, rprintf, tidyr, tidyverse/tidytemplate", + "Config/testthat/edition": "3", + "Config/usethis/last-upkeep": "2026-04-16", + "Encoding": "UTF-8", + "RoxygenNote": "7.3.3", + "NeedsCompilation": "yes", + "Author": "Jim Hester [aut] (ORCID: ), Jennifer Bryan [aut, cre] (ORCID: ), Posit Software, PBC [cph, fnd] (ROR: )", + "Maintainer": "Jennifer Bryan ", + "Repository": "CRAN" + }, + "hms": { + "Package": "hms", + "Version": "1.1.4", + "Source": "Repository", + "Title": "Pretty Time of Day", + "Date": "2025-10-11", + "Authors@R": "c( person(\"Kirill\", \"Müller\", , \"kirill@cynkra.com\", role = c(\"aut\", \"cre\"), comment = c(ORCID = \"0000-0002-1416-3412\")), person(\"R Consortium\", role = \"fnd\"), person(\"Posit Software, PBC\", role = \"fnd\", comment = c(ROR = \"03wc8by49\")) )", + "Description": "Implements an S3 class for storing and formatting time-of-day values, based on the 'difftime' class.", + "License": "MIT + file LICENSE", + "URL": "https://hms.tidyverse.org/, https://github.com/tidyverse/hms", + "BugReports": "https://github.com/tidyverse/hms/issues", + "Imports": [ + "cli", + "lifecycle", + "methods", + "pkgconfig", + "rlang (>= 1.0.2)", + "vctrs (>= 0.3.8)" + ], + "Suggests": [ + "crayon", + "lubridate", + "pillar (>= 1.1.0)", + "testthat (>= 3.0.0)" + ], + "Config/Needs/website": "tidyverse/tidytemplate", + "Config/testthat/edition": "3", + "Encoding": "UTF-8", + "RoxygenNote": "7.3.3.9000", + "NeedsCompilation": "no", + "Author": "Kirill Müller [aut, cre] (ORCID: ), R Consortium [fnd], Posit Software, PBC [fnd] (ROR: )", + "Maintainer": "Kirill Müller ", + "Repository": "CRAN" + }, + "htmltools": { + "Package": "htmltools", + "Version": "0.5.9", + "Source": "Repository", + "Type": "Package", + "Title": "Tools for HTML", + "Authors@R": "c( person(\"Joe\", \"Cheng\", , \"joe@posit.co\", role = \"aut\"), person(\"Carson\", \"Sievert\", , \"carson@posit.co\", role = c(\"aut\", \"cre\"), comment = c(ORCID = \"0000-0002-4958-2844\")), person(\"Barret\", \"Schloerke\", , \"barret@posit.co\", role = \"aut\", comment = c(ORCID = \"0000-0001-9986-114X\")), person(\"Winston\", \"Chang\", , \"winston@posit.co\", role = \"aut\", comment = c(ORCID = \"0000-0002-1576-2126\")), person(\"Yihui\", \"Xie\", , \"yihui@posit.co\", role = \"aut\"), person(\"Jeff\", \"Allen\", role = \"aut\"), person(\"Posit Software, PBC\", role = c(\"cph\", \"fnd\")) )", + "Description": "Tools for HTML generation and output.", + "License": "GPL (>= 2)", + "URL": "https://github.com/rstudio/htmltools, https://rstudio.github.io/htmltools/", + "BugReports": "https://github.com/rstudio/htmltools/issues", + "Depends": [ + "R (>= 2.14.1)" + ], + "Imports": [ + "base64enc", + "digest", + "fastmap (>= 1.1.0)", + "grDevices", + "rlang (>= 1.0.0)", + "utils" + ], + "Suggests": [ + "Cairo", + "markdown", + "ragg", + "shiny", + "testthat", + "withr" + ], + "Enhances": [ + "knitr" + ], + "Config/Needs/check": "knitr", + "Config/Needs/website": "rstudio/quillt, bench", + "Encoding": "UTF-8", + "RoxygenNote": "7.3.3", + "Collate": "'colors.R' 'fill.R' 'html_dependency.R' 'html_escape.R' 'html_print.R' 'htmltools-package.R' 'images.R' 'known_tags.R' 'selector.R' 'staticimports.R' 'tag_query.R' 'utils.R' 'tags.R' 'template.R'", + "NeedsCompilation": "yes", + "Author": "Joe Cheng [aut], Carson Sievert [aut, cre] (ORCID: ), Barret Schloerke [aut] (ORCID: ), Winston Chang [aut] (ORCID: ), Yihui Xie [aut], Jeff Allen [aut], Posit Software, PBC [cph, fnd]", + "Maintainer": "Carson Sievert ", + "Repository": "CRAN" + }, + "httpuv": { + "Package": "httpuv", + "Version": "1.6.17", + "Source": "Repository", + "Type": "Package", + "Title": "HTTP and WebSocket Server Library", + "Authors@R": "c( person(\"Joe\", \"Cheng\", , \"joe@posit.co\", role = \"aut\"), person(\"Winston\", \"Chang\", , \"winston@posit.co\", role = c(\"aut\", \"cre\")), person(\"Posit, PBC\", role = c(\"cph\", \"fnd\"), comment = c(ROR = \"03wc8by49\")), person(\"Hector\", \"Corrada Bravo\", role = \"ctb\"), person(\"Jeroen\", \"Ooms\", role = \"ctb\"), person(\"Andrzej\", \"Krzemienski\", role = \"cph\", comment = \"optional.hpp\"), person(\"libuv project contributors\", role = \"cph\", comment = \"libuv library, see src/libuv/AUTHORS file\"), person(\"Joyent, Inc. and other Node contributors\", role = \"cph\", comment = \"libuv library, see src/libuv/AUTHORS file; and http-parser library, see src/http-parser/AUTHORS file\"), person(\"Niels\", \"Provos\", role = \"cph\", comment = \"libuv subcomponent: tree.h\"), person(\"Internet Systems Consortium, Inc.\", role = \"cph\", comment = \"libuv subcomponent: inet_pton and inet_ntop, contained in src/libuv/src/inet.c\"), person(\"Alexander\", \"Chemeris\", role = \"cph\", comment = \"libuv subcomponent: stdint-msvc2008.h (from msinttypes)\"), person(\"Google, Inc.\", role = \"cph\", comment = \"libuv subcomponent: pthread-fixes.c\"), person(\"Sony Mobile Communcations AB\", role = \"cph\", comment = \"libuv subcomponent: pthread-fixes.c\"), person(\"Berkeley Software Design Inc.\", role = \"cph\", comment = \"libuv subcomponent: android-ifaddrs.h, android-ifaddrs.c\"), person(\"Kenneth\", \"MacKay\", role = \"cph\", comment = \"libuv subcomponent: android-ifaddrs.h, android-ifaddrs.c\"), person(\"Emergya (Cloud4all, FP7/2007-2013, grant agreement no 289016)\", role = \"cph\", comment = \"libuv subcomponent: android-ifaddrs.h, android-ifaddrs.c\"), person(\"Steve\", \"Reid\", role = \"aut\", comment = \"SHA-1 implementation\"), person(\"James\", \"Brown\", role = \"aut\", comment = \"SHA-1 implementation\"), person(\"Bob\", \"Trower\", role = \"aut\", comment = \"base64 implementation\"), person(\"Alexander\", \"Peslyak\", role = \"aut\", comment = \"MD5 implementation\"), person(\"Trantor Standard Systems\", role = \"cph\", comment = \"base64 implementation\"), person(\"Igor\", \"Sysoev\", role = \"cph\", comment = \"http-parser\") )", + "Description": "Provides low-level socket and protocol support for handling HTTP and WebSocket requests directly from within R. It is primarily intended as a building block for other packages, rather than making it particularly easy to create complete web applications using httpuv alone. httpuv is built on top of the libuv and http-parser C libraries, both of which were developed by Joyent, Inc. (See LICENSE file for libuv and http-parser license information.)", + "License": "GPL (>= 2) | file LICENSE", + "URL": "https://rstudio.github.io/httpuv/, https://github.com/rstudio/httpuv", + "BugReports": "https://github.com/rstudio/httpuv/issues", + "Depends": [ + "R (>= 2.15.1)" + ], + "Imports": [ + "later (>= 0.8.0)", + "promises", + "R6", + "Rcpp (>= 1.0.7)", + "utils" + ], + "Suggests": [ + "callr", + "curl", + "jsonlite", + "testthat (>= 3.0.0)", + "websocket" + ], + "LinkingTo": [ + "later", + "Rcpp" + ], + "Config/Needs/website": "tidyverse/tidytemplate", + "Config/testthat/edition": "3", + "Config/usethis/last-upkeep": "2025-07-01", + "Encoding": "UTF-8", + "RoxygenNote": "7.3.3", + "SystemRequirements": "GNU make, zlib", + "Collate": "'RcppExports.R' 'httpuv-package.R' 'httpuv.R' 'random_port.R' 'server.R' 'staticServer.R' 'static_paths.R' 'utils.R'", + "NeedsCompilation": "yes", + "Author": "Joe Cheng [aut], Winston Chang [aut, cre], Posit, PBC [cph, fnd] (ROR: ), Hector Corrada Bravo [ctb], Jeroen Ooms [ctb], Andrzej Krzemienski [cph] (optional.hpp), libuv project contributors [cph] (libuv library, see src/libuv/AUTHORS file), Joyent, Inc. and other Node contributors [cph] (libuv library, see src/libuv/AUTHORS file; and http-parser library, see src/http-parser/AUTHORS file), Niels Provos [cph] (libuv subcomponent: tree.h), Internet Systems Consortium, Inc. [cph] (libuv subcomponent: inet_pton and inet_ntop, contained in src/libuv/src/inet.c), Alexander Chemeris [cph] (libuv subcomponent: stdint-msvc2008.h (from msinttypes)), Google, Inc. [cph] (libuv subcomponent: pthread-fixes.c), Sony Mobile Communcations AB [cph] (libuv subcomponent: pthread-fixes.c), Berkeley Software Design Inc. [cph] (libuv subcomponent: android-ifaddrs.h, android-ifaddrs.c), Kenneth MacKay [cph] (libuv subcomponent: android-ifaddrs.h, android-ifaddrs.c), Emergya (Cloud4all, FP7/2007-2013, grant agreement no 289016) [cph] (libuv subcomponent: android-ifaddrs.h, android-ifaddrs.c), Steve Reid [aut] (SHA-1 implementation), James Brown [aut] (SHA-1 implementation), Bob Trower [aut] (base64 implementation), Alexander Peslyak [aut] (MD5 implementation), Trantor Standard Systems [cph] (base64 implementation), Igor Sysoev [cph] (http-parser)", + "Maintainer": "Winston Chang ", + "Repository": "CRAN" + }, + "jquerylib": { + "Package": "jquerylib", + "Version": "0.1.4", + "Source": "Repository", + "Title": "Obtain 'jQuery' as an HTML Dependency Object", + "Authors@R": "c( person(\"Carson\", \"Sievert\", role = c(\"aut\", \"cre\"), email = \"carson@rstudio.com\", comment = c(ORCID = \"0000-0002-4958-2844\")), person(\"Joe\", \"Cheng\", role = \"aut\", email = \"joe@rstudio.com\"), person(family = \"RStudio\", role = \"cph\"), person(family = \"jQuery Foundation\", role = \"cph\", comment = \"jQuery library and jQuery UI library\"), person(family = \"jQuery contributors\", role = c(\"ctb\", \"cph\"), comment = \"jQuery library; authors listed in inst/lib/jquery-AUTHORS.txt\") )", + "Description": "Obtain any major version of 'jQuery' () and use it in any webpage generated by 'htmltools' (e.g. 'shiny', 'htmlwidgets', and 'rmarkdown'). Most R users don't need to use this package directly, but other R packages (e.g. 'shiny', 'rmarkdown', etc.) depend on this package to avoid bundling redundant copies of 'jQuery'.", + "License": "MIT + file LICENSE", + "Encoding": "UTF-8", + "Config/testthat/edition": "3", + "RoxygenNote": "7.0.2", + "Imports": [ + "htmltools" + ], + "Suggests": [ + "testthat" + ], + "NeedsCompilation": "no", + "Author": "Carson Sievert [aut, cre] (), Joe Cheng [aut], RStudio [cph], jQuery Foundation [cph] (jQuery library and jQuery UI library), jQuery contributors [ctb, cph] (jQuery library; authors listed in inst/lib/jquery-AUTHORS.txt)", + "Maintainer": "Carson Sievert ", + "Repository": "CRAN" + }, + "jsonlite": { + "Package": "jsonlite", + "Version": "2.0.0", + "Source": "Repository", + "Title": "A Simple and Robust JSON Parser and Generator for R", + "License": "MIT + file LICENSE", + "Depends": [ + "methods" + ], + "Authors@R": "c( person(\"Jeroen\", \"Ooms\", role = c(\"aut\", \"cre\"), email = \"jeroenooms@gmail.com\", comment = c(ORCID = \"0000-0002-4035-0289\")), person(\"Duncan\", \"Temple Lang\", role = \"ctb\"), person(\"Lloyd\", \"Hilaiel\", role = \"cph\", comment=\"author of bundled libyajl\"))", + "URL": "https://jeroen.r-universe.dev/jsonlite https://arxiv.org/abs/1403.2805", + "BugReports": "https://github.com/jeroen/jsonlite/issues", + "Maintainer": "Jeroen Ooms ", + "VignetteBuilder": "knitr, R.rsp", + "Description": "A reasonably fast JSON parser and generator, optimized for statistical data and the web. Offers simple, flexible tools for working with JSON in R, and is particularly powerful for building pipelines and interacting with a web API. The implementation is based on the mapping described in the vignette (Ooms, 2014). In addition to converting JSON data from/to R objects, 'jsonlite' contains functions to stream, validate, and prettify JSON data. The unit tests included with the package verify that all edge cases are encoded and decoded consistently for use with dynamic data in systems and applications.", + "Suggests": [ + "httr", + "vctrs", + "testthat", + "knitr", + "rmarkdown", + "R.rsp", + "sf" + ], + "RoxygenNote": "7.3.2", + "Encoding": "UTF-8", + "NeedsCompilation": "yes", + "Author": "Jeroen Ooms [aut, cre] (), Duncan Temple Lang [ctb], Lloyd Hilaiel [cph] (author of bundled libyajl)", + "Repository": "CRAN" + }, + "later": { + "Package": "later", + "Version": "1.4.8", + "Source": "Repository", + "Type": "Package", + "Title": "Utilities for Scheduling Functions to Execute Later with Event Loops", + "Authors@R": "c( person(\"Winston\", \"Chang\", , \"winston@posit.co\", role = \"aut\", comment = c(ORCID = \"0000-0002-1576-2126\")), person(\"Joe\", \"Cheng\", , \"joe@posit.co\", role = \"aut\"), person(\"Charlie\", \"Gao\", , \"charlie.gao@posit.co\", role = c(\"aut\", \"cre\"), comment = c(ORCID = \"0000-0002-0750-061X\")), person(\"Posit Software, PBC\", role = c(\"cph\", \"fnd\"), comment = c(ROR = \"03wc8by49\")), person(\"Marcus\", \"Geelnard\", role = c(\"ctb\", \"cph\"), comment = \"TinyCThread library, https://tinycthread.github.io/\"), person(\"Evan\", \"Nemerson\", role = c(\"ctb\", \"cph\"), comment = \"TinyCThread library, https://tinycthread.github.io/\") )", + "Description": "Executes arbitrary R or C functions some time after the current time, after the R execution stack has emptied. The functions are scheduled in an event loop.", + "License": "MIT + file LICENSE", + "URL": "https://later.r-lib.org, https://github.com/r-lib/later", + "BugReports": "https://github.com/r-lib/later/issues", + "Depends": [ + "R (>= 3.5)" + ], + "Imports": [ + "Rcpp (>= 1.0.10)", + "rlang" + ], + "Suggests": [ + "knitr", + "nanonext", + "promises", + "rmarkdown", + "testthat (>= 3.0.0)" + ], + "LinkingTo": [ + "Rcpp" + ], + "VignetteBuilder": "knitr", + "Config/build/compilation-database": "true", + "Config/Needs/website": "tidyverse/tidytemplate", + "Config/testthat/edition": "3", + "Config/usethis/last-upkeep": "2025-07-18", + "Encoding": "UTF-8", + "RoxygenNote": "7.3.3", + "NeedsCompilation": "yes", + "Author": "Winston Chang [aut] (ORCID: ), Joe Cheng [aut], Charlie Gao [aut, cre] (ORCID: ), Posit Software, PBC [cph, fnd] (ROR: ), Marcus Geelnard [ctb, cph] (TinyCThread library, https://tinycthread.github.io/), Evan Nemerson [ctb, cph] (TinyCThread library, https://tinycthread.github.io/)", + "Maintainer": "Charlie Gao ", + "Repository": "CRAN" + }, + "lattice": { + "Package": "lattice", + "Version": "0.22-9", + "Source": "Repository", + "Date": "2026-02-03", + "Priority": "recommended", + "Title": "Trellis Graphics for R", + "Authors@R": "c(person(\"Deepayan\", \"Sarkar\", role = c(\"aut\", \"cre\"), email = \"deepayan.sarkar@r-project.org\", comment = c(ORCID = \"0000-0003-4107-1553\")), person(\"Felix\", \"Andrews\", role = \"ctb\"), person(\"Kevin\", \"Wright\", role = \"ctb\", comment = \"documentation\"), person(\"Neil\", \"Klepeis\", role = \"ctb\"), person(\"Johan\", \"Larsson\", role = \"ctb\", comment = \"miscellaneous improvements\"), person(\"Zhijian (Jason)\", \"Wen\", role = \"cph\", comment = \"filled contour code\"), person(\"Paul\", \"Murrell\", role = \"ctb\", email = \"paul@stat.auckland.ac.nz\"), person(\"Stefan\", \"Eng\", role = \"ctb\", comment = \"violin plot improvements\"), person(\"Achim\", \"Zeileis\", role = \"ctb\", comment = \"modern colors\"), person(\"Alexandre\", \"Courtiol\", role = \"ctb\", comment = \"generics for larrows, lpolygon, lrect and lsegments\") )", + "Description": "A powerful and elegant high-level data visualization system inspired by Trellis graphics, with an emphasis on multivariate data. Lattice is sufficient for typical graphics needs, and is also flexible enough to handle most nonstandard requirements. See ?Lattice for an introduction.", + "Depends": [ + "R (>= 4.0.0)" + ], + "Suggests": [ + "KernSmooth", + "MASS", + "latticeExtra", + "colorspace" + ], + "Imports": [ + "grid", + "grDevices", + "graphics", + "stats", + "utils" + ], + "Enhances": [ + "chron", + "zoo" + ], + "LazyLoad": "yes", + "LazyData": "yes", + "License": "GPL (>= 2)", + "URL": "https://lattice.r-forge.r-project.org/", + "BugReports": "https://github.com/deepayan/lattice/issues", + "NeedsCompilation": "yes", + "Author": "Deepayan Sarkar [aut, cre] (ORCID: ), Felix Andrews [ctb], Kevin Wright [ctb] (documentation), Neil Klepeis [ctb], Johan Larsson [ctb] (miscellaneous improvements), Zhijian (Jason) Wen [cph] (filled contour code), Paul Murrell [ctb], Stefan Eng [ctb] (violin plot improvements), Achim Zeileis [ctb] (modern colors), Alexandre Courtiol [ctb] (generics for larrows, lpolygon, lrect and lsegments)", + "Maintainer": "Deepayan Sarkar ", + "Repository": "CRAN" + }, + "lifecycle": { + "Package": "lifecycle", + "Version": "1.0.5", + "Source": "Repository", + "Title": "Manage the Life Cycle of your Package Functions", + "Authors@R": "c( person(\"Lionel\", \"Henry\", , \"lionel@posit.co\", role = c(\"aut\", \"cre\")), person(\"Hadley\", \"Wickham\", , \"hadley@posit.co\", role = \"aut\", comment = c(ORCID = \"0000-0003-4757-117X\")), person(\"Posit Software, PBC\", role = c(\"cph\", \"fnd\")) )", + "Description": "Manage the life cycle of your exported functions with shared conventions, documentation badges, and user-friendly deprecation warnings.", + "License": "MIT + file LICENSE", + "URL": "https://lifecycle.r-lib.org/, https://github.com/r-lib/lifecycle", + "BugReports": "https://github.com/r-lib/lifecycle/issues", + "Depends": [ + "R (>= 3.6)" + ], + "Imports": [ + "cli (>= 3.4.0)", + "rlang (>= 1.1.0)" + ], + "Suggests": [ + "covr", + "knitr", + "lintr (>= 3.1.0)", + "rmarkdown", + "testthat (>= 3.0.1)", + "tibble", + "tidyverse", + "tools", + "vctrs", + "withr", + "xml2" + ], + "VignetteBuilder": "knitr", + "Config/Needs/website": "tidyverse/tidytemplate, usethis", + "Config/testthat/edition": "3", + "Encoding": "UTF-8", + "RoxygenNote": "7.3.3", + "NeedsCompilation": "no", + "Author": "Lionel Henry [aut, cre], Hadley Wickham [aut] (ORCID: ), Posit Software, PBC [cph, fnd]", + "Maintainer": "Lionel Henry ", + "Repository": "CRAN" + }, + "lubridate": { + "Package": "lubridate", + "Version": "1.9.5", + "Source": "Repository", + "Type": "Package", + "Title": "Make Dealing with Dates a Little Easier", + "Authors@R": "c( person(\"Vitalie\", \"Spinu\", , \"spinuvit@gmail.com\", role = c(\"aut\", \"cre\")), person(\"Garrett\", \"Grolemund\", role = \"aut\"), person(\"Hadley\", \"Wickham\", role = \"aut\"), person(\"Davis\", \"Vaughan\", role = \"ctb\"), person(\"Ian\", \"Lyttle\", role = \"ctb\"), person(\"Imanuel\", \"Costigan\", role = \"ctb\"), person(\"Jason\", \"Law\", role = \"ctb\"), person(\"Doug\", \"Mitarotonda\", role = \"ctb\"), person(\"Joseph\", \"Larmarange\", role = \"ctb\"), person(\"Jonathan\", \"Boiser\", role = \"ctb\"), person(\"Chel Hee\", \"Lee\", role = \"ctb\") )", + "Maintainer": "Vitalie Spinu ", + "Description": "Functions to work with date-times and time-spans: fast and user friendly parsing of date-time data, extraction and updating of components of a date-time (years, months, days, hours, minutes, and seconds), algebraic manipulation on date-time and time-span objects. The 'lubridate' package has a consistent and memorable syntax that makes working with dates easy and fun.", + "License": "MIT + file LICENSE", + "URL": "https://lubridate.tidyverse.org, https://github.com/tidyverse/lubridate", + "BugReports": "https://github.com/tidyverse/lubridate/issues", + "Depends": [ + "methods", + "R (>= 3.2)" + ], + "Imports": [ + "generics", + "timechange (>= 0.4.0)" + ], + "Suggests": [ + "covr", + "knitr", + "rmarkdown", + "testthat (>= 2.1.0)", + "vctrs (>= 0.6.5)" + ], + "Enhances": [ + "chron", + "data.table", + "timeDate", + "tis", + "zoo" + ], + "VignetteBuilder": "knitr", + "Config/Needs/website": "tidyverse/tidytemplate", + "Config/testthat/edition": "3", + "Encoding": "UTF-8", + "LazyData": "true", + "RoxygenNote": "7.3.3", + "SystemRequirements": "A system with zoneinfo data (e.g. /usr/share/zoneinfo). On Windows the zoneinfo included with R is used.", + "Collate": "'Dates.r' 'POSIXt.r' 'util.r' 'parse.r' 'timespans.r' 'intervals.r' 'difftimes.r' 'durations.r' 'periods.r' 'accessors-date.R' 'accessors-day.r' 'accessors-dst.r' 'accessors-hour.r' 'accessors-minute.r' 'accessors-month.r' 'accessors-quarter.r' 'accessors-second.r' 'accessors-tz.r' 'accessors-week.r' 'accessors-year.r' 'am-pm.r' 'time-zones.r' 'numeric.r' 'coercion.r' 'constants.r' 'cyclic_encoding.r' 'data.r' 'decimal-dates.r' 'deprecated.r' 'format_ISO8601.r' 'guess.r' 'hidden.r' 'instants.r' 'leap-years.r' 'ops-addition.r' 'ops-compare.r' 'ops-division.r' 'ops-integer-division.r' 'ops-m+.r' 'ops-modulo.r' 'ops-multiplication.r' 'ops-subtraction.r' 'package.r' 'pretty.r' 'round.r' 'stamp.r' 'tzdir.R' 'update.r' 'vctrs.R' 'zzz.R'", + "NeedsCompilation": "yes", + "Author": "Vitalie Spinu [aut, cre], Garrett Grolemund [aut], Hadley Wickham [aut], Davis Vaughan [ctb], Ian Lyttle [ctb], Imanuel Costigan [ctb], Jason Law [ctb], Doug Mitarotonda [ctb], Joseph Larmarange [ctb], Jonathan Boiser [ctb], Chel Hee Lee [ctb]", + "Repository": "CRAN" + }, + "magrittr": { + "Package": "magrittr", + "Version": "2.0.5", + "Source": "Repository", + "Type": "Package", + "Title": "A Forward-Pipe Operator for R", + "Authors@R": "c( person(\"Stefan Milton\", \"Bache\", , \"stefan@stefanbache.dk\", role = c(\"aut\", \"cph\"), comment = \"Original author and creator of magrittr\"), person(\"Hadley\", \"Wickham\", , \"hadley@posit.co\", role = \"aut\"), person(\"Lionel\", \"Henry\", , \"lionel@posit.co\", role = \"cre\"), person(\"Posit Software, PBC\", role = c(\"cph\", \"fnd\"), comment = c(ROR = \"03wc8by49\")) )", + "Description": "Provides a mechanism for chaining commands with a new forward-pipe operator, %>%. This operator will forward a value, or the result of an expression, into the next function call/expression. There is flexible support for the type of right-hand side expressions. For more information, see package vignette. To quote Rene Magritte, \"Ceci n'est pas un pipe.\"", + "License": "MIT + file LICENSE", + "URL": "https://magrittr.tidyverse.org, https://github.com/tidyverse/magrittr", + "BugReports": "https://github.com/tidyverse/magrittr/issues", + "Depends": [ + "R (>= 3.4.0)" + ], + "Suggests": [ + "covr", + "knitr", + "rlang", + "rmarkdown", + "testthat" + ], + "VignetteBuilder": "knitr", + "ByteCompile": "Yes", + "Config/Needs/website": "tidyverse/tidytemplate", + "Encoding": "UTF-8", + "RoxygenNote": "7.3.3", + "NeedsCompilation": "yes", + "Author": "Stefan Milton Bache [aut, cph] (Original author and creator of magrittr), Hadley Wickham [aut], Lionel Henry [cre], Posit Software, PBC [cph, fnd] (ROR: )", + "Maintainer": "Lionel Henry ", + "Repository": "CRAN" + }, + "memoise": { + "Package": "memoise", + "Version": "2.0.1", + "Source": "Repository", + "Title": "'Memoisation' of Functions", + "Authors@R": "c(person(given = \"Hadley\", family = \"Wickham\", role = \"aut\", email = \"hadley@rstudio.com\"), person(given = \"Jim\", family = \"Hester\", role = \"aut\"), person(given = \"Winston\", family = \"Chang\", role = c(\"aut\", \"cre\"), email = \"winston@rstudio.com\"), person(given = \"Kirill\", family = \"Müller\", role = \"aut\", email = \"krlmlr+r@mailbox.org\"), person(given = \"Daniel\", family = \"Cook\", role = \"aut\", email = \"danielecook@gmail.com\"), person(given = \"Mark\", family = \"Edmondson\", role = \"ctb\", email = \"r@sunholo.com\"))", + "Description": "Cache the results of a function so that when you call it again with the same arguments it returns the previously computed value.", + "License": "MIT + file LICENSE", + "URL": "https://memoise.r-lib.org, https://github.com/r-lib/memoise", + "BugReports": "https://github.com/r-lib/memoise/issues", + "Imports": [ + "rlang (>= 0.4.10)", + "cachem" + ], + "Suggests": [ + "digest", + "aws.s3", + "covr", + "googleAuthR", + "googleCloudStorageR", + "httr", + "testthat" + ], + "Encoding": "UTF-8", + "RoxygenNote": "7.1.2", + "NeedsCompilation": "no", + "Author": "Hadley Wickham [aut], Jim Hester [aut], Winston Chang [aut, cre], Kirill Müller [aut], Daniel Cook [aut], Mark Edmondson [ctb]", + "Maintainer": "Winston Chang ", + "Repository": "CRAN" + }, + "mgcv": { + "Package": "mgcv", + "Version": "1.9-4", + "Source": "Repository", + "Authors@R": "person(given = \"Simon\", family = \"Wood\", role = c(\"aut\", \"cre\"), email = \"simon.wood@r-project.org\")", + "Title": "Mixed GAM Computation Vehicle with Automatic Smoothness Estimation", + "Description": "Generalized additive (mixed) models, some of their extensions and other generalized ridge regression with multiple smoothing parameter estimation by (Restricted) Marginal Likelihood, Cross Validation and similar, or using iterated nested Laplace approximation for fully Bayesian inference. See Wood (2025) for an overview. Includes a gam() function, a wide variety of smoothers, 'JAGS' support and distributions beyond the exponential family.", + "Priority": "recommended", + "Depends": [ + "R (>= 4.4.0)", + "nlme (>= 3.1-64)" + ], + "Imports": [ + "methods", + "stats", + "graphics", + "Matrix", + "splines", + "utils" + ], + "Suggests": [ + "parallel", + "survival", + "MASS" + ], + "LazyLoad": "yes", + "ByteCompile": "yes", + "License": "GPL (>= 2)", + "NeedsCompilation": "yes", + "Author": "Simon Wood [aut, cre]", + "Maintainer": "Simon Wood ", + "Repository": "CRAN" + }, + "mime": { + "Package": "mime", + "Version": "0.13", + "Source": "Repository", + "Type": "Package", + "Title": "Map Filenames to MIME Types", + "Authors@R": "c( person(\"Yihui\", \"Xie\", role = c(\"aut\", \"cre\"), email = \"xie@yihui.name\", comment = c(ORCID = \"0000-0003-0645-5666\", URL = \"https://yihui.org\")), person(\"Jeffrey\", \"Horner\", role = \"ctb\"), person(\"Beilei\", \"Bian\", role = \"ctb\") )", + "Description": "Guesses the MIME type from a filename extension using the data derived from /etc/mime.types in UNIX-type systems.", + "Imports": [ + "tools" + ], + "License": "GPL", + "URL": "https://github.com/yihui/mime", + "BugReports": "https://github.com/yihui/mime/issues", + "RoxygenNote": "7.3.2", + "Encoding": "UTF-8", + "NeedsCompilation": "yes", + "Author": "Yihui Xie [aut, cre] (, https://yihui.org), Jeffrey Horner [ctb], Beilei Bian [ctb]", + "Maintainer": "Yihui Xie ", + "Repository": "CRAN" + }, + "nlme": { + "Package": "nlme", + "Version": "3.1-169", + "Source": "Repository", + "Date": "2026-03-27", + "Priority": "recommended", + "Title": "Linear and Nonlinear Mixed Effects Models", + "Authors@R": "c(person(\"José\", \"Pinheiro\", role = \"aut\", comment = \"S version\"), person(\"Douglas\", \"Bates\", role = \"aut\", comment = \"up to 2007\"), person(\"Saikat\", \"DebRoy\", role = \"ctb\", comment = \"up to 2002\"), person(\"Deepayan\", \"Sarkar\", role = \"ctb\", comment = \"up to 2005\"), person(\"EISPACK authors\", role = \"ctb\", comment = \"src/rs.f\"), person(\"Siem\", \"Heisterkamp\", role = \"ctb\", comment = \"Author fixed sigma\"), person(\"Bert\", \"Van Willigen\",role = \"ctb\", comment = \"Programmer fixed sigma\"), person(\"Johannes\", \"Ranke\", role = \"ctb\", comment = \"varConstProp()\"), person(\"R Core Team\", email = \"R-core@R-project.org\", role = c(\"aut\", \"cre\"), comment = c(ROR = \"02zz1nj61\")))", + "Contact": "see 'MailingList'", + "Description": "Fit and compare Gaussian linear and nonlinear mixed-effects models.", + "Depends": [ + "R (>= 3.6.0)" + ], + "Imports": [ + "graphics", + "stats", + "utils", + "lattice" + ], + "Suggests": [ + "MASS", + "SASmixed" + ], + "LazyData": "yes", + "Encoding": "UTF-8", + "License": "GPL (>= 2)", + "BugReports": "https://bugs.r-project.org", + "MailingList": "R-help@r-project.org", + "URL": "https://svn.r-project.org/R-packages/trunk/nlme/", + "NeedsCompilation": "yes", + "Author": "José Pinheiro [aut] (S version), Douglas Bates [aut] (up to 2007), Saikat DebRoy [ctb] (up to 2002), Deepayan Sarkar [ctb] (up to 2005), EISPACK authors [ctb] (src/rs.f), Siem Heisterkamp [ctb] (Author fixed sigma), Bert Van Willigen [ctb] (Programmer fixed sigma), Johannes Ranke [ctb] (varConstProp()), R Core Team [aut, cre] (ROR: )", + "Maintainer": "R Core Team ", + "Repository": "CRAN" + }, + "nnet": { + "Package": "nnet", + "Version": "7.3-20", + "Source": "Repository", + "Priority": "recommended", + "Date": "2025-01-01", + "Depends": [ + "R (>= 3.0.0)", + "stats", + "utils" + ], + "Suggests": [ + "MASS" + ], + "Authors@R": "c(person(\"Brian\", \"Ripley\", role = c(\"aut\", \"cre\", \"cph\"), email = \"Brian.Ripley@R-project.org\"), person(\"William\", \"Venables\", role = \"cph\"))", + "Description": "Software for feed-forward neural networks with a single hidden layer, and for multinomial log-linear models.", + "Title": "Feed-Forward Neural Networks and Multinomial Log-Linear Models", + "ByteCompile": "yes", + "License": "GPL-2 | GPL-3", + "URL": "http://www.stats.ox.ac.uk/pub/MASS4/", + "NeedsCompilation": "yes", + "Author": "Brian Ripley [aut, cre, cph], William Venables [cph]", + "Maintainer": "Brian Ripley ", + "Repository": "CRAN" + }, + "otel": { + "Package": "otel", + "Version": "0.2.0", + "Source": "Repository", + "Title": "OpenTelemetry R API", + "Authors@R": "person(\"Gábor\", \"Csárdi\", , \"csardi.gabor@gmail.com\", role = c(\"aut\", \"cre\"))", + "Description": "High-quality, ubiquitous, and portable telemetry to enable effective observability. OpenTelemetry is a collection of tools, APIs, and SDKs used to instrument, generate, collect, and export telemetry data (metrics, logs, and traces) for analysis in order to understand your software's performance and behavior. This package implements the OpenTelemetry API: . Use this package as a dependency if you want to instrument your R package for OpenTelemetry.", + "License": "MIT + file LICENSE", + "Encoding": "UTF-8", + "RoxygenNote": "7.3.2.9000", + "Depends": [ + "R (>= 3.6.0)" + ], + "Suggests": [ + "callr", + "cli", + "glue", + "jsonlite", + "otelsdk", + "processx", + "shiny", + "spelling", + "testthat (>= 3.0.0)", + "utils", + "withr" + ], + "Config/Needs/website": "tidyverse/tidytemplate", + "Config/testthat/edition": "3", + "URL": "https://otel.r-lib.org, https://github.com/r-lib/otel", + "Additional_repositories": "https://github.com/r-lib/otelsdk/releases/download/devel", + "BugReports": "https://github.com/r-lib/otel/issues", + "NeedsCompilation": "no", + "Author": "Gábor Csárdi [aut, cre]", + "Maintainer": "Gábor Csárdi ", + "Repository": "CRAN" + }, + "pkgconfig": { + "Package": "pkgconfig", + "Version": "2.0.3", + "Source": "Repository", + "Title": "Private Configuration for 'R' Packages", + "Author": "Gábor Csárdi", + "Maintainer": "Gábor Csárdi ", + "Description": "Set configuration options on a per-package basis. Options set by a given package only apply to that package, other packages are unaffected.", + "License": "MIT + file LICENSE", + "LazyData": "true", + "Imports": [ + "utils" + ], + "Suggests": [ + "covr", + "testthat", + "disposables (>= 1.0.3)" + ], + "URL": "https://github.com/r-lib/pkgconfig#readme", + "BugReports": "https://github.com/r-lib/pkgconfig/issues", + "Encoding": "UTF-8", + "NeedsCompilation": "no", + "Repository": "CRAN" + }, + "pool": { + "Package": "pool", + "Version": "1.0.5", + "Source": "Repository", + "Type": "Package", + "Title": "Object Pooling", + "Authors@R": "c( person(\"Joe\", \"Cheng\", , \"joe@posit.co\", role = \"aut\"), person(\"Barbara\", \"Borges\", role = \"aut\"), person(\"Hadley\", \"Wickham\", , \"hadley@posit.co\", role = c(\"aut\", \"cre\")), person(\"Posit Software, PBC\", role = c(\"cph\", \"fnd\")) )", + "Description": "Enables the creation of object pools, which make it less computationally expensive to fetch a new object. Currently the only supported pooled objects are 'DBI' connections.", + "License": "MIT + file LICENSE", + "URL": "https://github.com/rstudio/pool, https://rstudio.github.io/pool/", + "BugReports": "https://github.com/rstudio/pool/issues", + "Depends": [ + "methods", + "R (>= 3.6.0)" + ], + "Imports": [ + "DBI (>= 1.2.1)", + "later (>= 1.0.0)", + "R6", + "rlang (>= 1.0.0)" + ], + "Suggests": [ + "covr", + "dbplyr (>= 2.4.0)", + "dplyr", + "knitr", + "rmarkdown", + "RSQLite", + "shiny", + "testthat (>= 3.0.0)", + "tibble" + ], + "VignetteBuilder": "knitr", + "Config/Needs/website": "tidyverse/tidytemplate", + "Config/roxygen2/version": "8.0.0", + "Config/testthat/edition": "3", + "Encoding": "UTF-8", + "NeedsCompilation": "no", + "Author": "Joe Cheng [aut], Barbara Borges [aut], Hadley Wickham [aut, cre], Posit Software, PBC [cph, fnd]", + "Maintainer": "Hadley Wickham ", + "Repository": "CRAN" + }, + "promises": { + "Package": "promises", + "Version": "1.5.0", + "Source": "Repository", + "Type": "Package", + "Title": "Abstractions for Promise-Based Asynchronous Programming", + "Authors@R": "c( person(\"Joe\", \"Cheng\", , \"joe@posit.co\", role = \"aut\"), person(\"Barret\", \"Schloerke\", , \"barret@posit.co\", role = c(\"aut\", \"cre\"), comment = c(ORCID = \"0000-0001-9986-114X\")), person(\"Winston\", \"Chang\", , \"winston@posit.co\", role = \"aut\", comment = c(ORCID = \"0000-0002-1576-2126\")), person(\"Charlie\", \"Gao\", , \"charlie.gao@posit.co\", role = \"aut\", comment = c(ORCID = \"0000-0002-0750-061X\")), person(\"Posit Software, PBC\", role = c(\"cph\", \"fnd\"), comment = c(ROR = \"03wc8by49\")) )", + "Description": "Provides fundamental abstractions for doing asynchronous programming in R using promises. Asynchronous programming is useful for allowing a single R process to orchestrate multiple tasks in the background while also attending to something else. Semantics are similar to 'JavaScript' promises, but with a syntax that is idiomatic R.", + "License": "MIT + file LICENSE", + "URL": "https://rstudio.github.io/promises/, https://github.com/rstudio/promises", + "BugReports": "https://github.com/rstudio/promises/issues", + "Depends": [ + "R (>= 4.1.0)" + ], + "Imports": [ + "fastmap (>= 1.1.0)", + "later", + "lifecycle", + "magrittr (>= 1.5)", + "otel (>= 0.2.0)", + "R6", + "rlang" + ], + "Suggests": [ + "future (>= 1.21.0)", + "knitr", + "mirai", + "otelsdk (>= 0.2.0)", + "purrr", + "Rcpp", + "rmarkdown", + "spelling", + "testthat (>= 3.0.0)", + "vembedr" + ], + "VignetteBuilder": "knitr", + "Config/Needs/website": "rsconnect, tidyverse/tidytemplate", + "Config/testthat/edition": "3", + "Config/usethis/last-upkeep": "2025-05-27", + "Encoding": "UTF-8", + "Language": "en-US", + "RoxygenNote": "7.3.3", + "NeedsCompilation": "no", + "Author": "Joe Cheng [aut], Barret Schloerke [aut, cre] (ORCID: ), Winston Chang [aut] (ORCID: ), Charlie Gao [aut] (ORCID: ), Posit Software, PBC [cph, fnd] (ROR: )", + "Maintainer": "Barret Schloerke ", + "Repository": "CRAN" + }, + "rappdirs": { + "Package": "rappdirs", + "Version": "0.3.4", + "Source": "Repository", + "Type": "Package", + "Title": "Application Directories: Determine Where to Save Data, Caches, and Logs", + "Authors@R": "c( person(\"Hadley\", \"Wickham\", , \"hadley@posit.co\", role = c(\"trl\", \"cre\", \"cph\")), person(\"Sridhar\", \"Ratnakumar\", role = \"aut\"), person(\"Trent\", \"Mick\", role = \"aut\"), person(\"ActiveState\", role = \"cph\", comment = \"R/appdir.r, R/cache.r, R/data.r, R/log.r translated from appdirs\"), person(\"Eddy\", \"Petrisor\", role = \"ctb\"), person(\"Trevor\", \"Davis\", role = c(\"trl\", \"aut\"), comment = c(ORCID = \"0000-0001-6341-4639\")), person(\"Gabor\", \"Csardi\", role = \"ctb\"), person(\"Gregory\", \"Jefferis\", role = \"ctb\"), person(\"Posit Software, PBC\", role = c(\"cph\", \"fnd\"), comment = c(ROR = \"03wc8by49\")) )", + "Description": "An easy way to determine which directories on the users computer you should use to save data, caches and logs. A port of Python's 'Appdirs' () to R.", + "License": "MIT + file LICENSE", + "URL": "https://rappdirs.r-lib.org, https://github.com/r-lib/rappdirs", + "BugReports": "https://github.com/r-lib/rappdirs/issues", + "Depends": [ + "R (>= 4.1)" + ], + "Suggests": [ + "covr", + "roxygen2", + "testthat (>= 3.2.0)", + "withr" + ], + "Config/Needs/website": "tidyverse/tidytemplate", + "Config/testthat/edition": "3", + "Config/usethis/last-upkeep": "2025-05-05", + "Copyright": "Original python appdirs module copyright (c) 2010 ActiveState Software Inc. R port copyright Hadley Wickham, Posit, PBC. See file LICENSE for details.", + "Encoding": "UTF-8", + "RoxygenNote": "7.3.3", + "NeedsCompilation": "yes", + "Author": "Hadley Wickham [trl, cre, cph], Sridhar Ratnakumar [aut], Trent Mick [aut], ActiveState [cph] (R/appdir.r, R/cache.r, R/data.r, R/log.r translated from appdirs), Eddy Petrisor [ctb], Trevor Davis [trl, aut] (ORCID: ), Gabor Csardi [ctb], Gregory Jefferis [ctb], Posit Software, PBC [cph, fnd] (ROR: )", + "Maintainer": "Hadley Wickham ", + "Repository": "CRAN" + }, + "renv": { + "Package": "renv", + "Version": "1.3.0", + "Source": "Repository", + "Type": "Package", + "Title": "Project Environments", + "Authors@R": "c( person(\"Kevin\", \"Ushey\", role = c(\"aut\", \"cre\"), email = \"kevin@rstudio.com\", comment = c(ORCID = \"0000-0003-2880-7407\")), person(\"Hadley\", \"Wickham\", role = c(\"aut\"), email = \"hadley@rstudio.com\", comment = c(ORCID = \"0000-0003-4757-117X\")), person(\"Posit Software, PBC\", role = c(\"cph\", \"fnd\")) )", + "Description": "A dependency management toolkit for R. Using 'renv', you can create and manage project-local R libraries, save the state of these libraries to a 'lockfile', and later restore your library as required. Together, these tools can help make your projects more isolated, portable, and reproducible.", + "License": "MIT + file LICENSE", + "URL": "https://rstudio.github.io/renv/, https://github.com/rstudio/renv", + "BugReports": "https://github.com/rstudio/renv/issues", + "Imports": [ + "utils" + ], + "Suggests": [ + "BiocManager", + "cli", + "compiler", + "covr", + "cpp11", + "curl", + "devtools", + "generics", + "gitcreds", + "jsonlite", + "jsonvalidate", + "knitr", + "miniUI", + "modules", + "packrat", + "pak", + "pkgload", + "R6", + "remotes", + "reticulate", + "rmarkdown", + "rstudioapi", + "shiny", + "testthat", + "uuid", + "waldo", + "yaml", + "webfakes" + ], + "Encoding": "UTF-8", + "VignetteBuilder": "knitr", + "Config/Needs/website": "tidyverse/tidytemplate", + "Config/testthat/edition": "3", + "Config/testthat/parallel": "true", + "Config/testthat/start-first": "bioconductor,python,install,restore,snapshot,retrieve,remotes", + "Config/roxygen2/version": "8.0.0", + "NeedsCompilation": "no", + "Author": "Kevin Ushey [aut, cre] (ORCID: ), Hadley Wickham [aut] (ORCID: ), Posit Software, PBC [cph, fnd]", + "Maintainer": "Kevin Ushey ", + "Repository": "CRAN" + }, + "rlang": { + "Package": "rlang", + "Version": "1.3.0", + "Source": "Repository", + "Title": "Functions for Base Types and Core R and 'Tidyverse' Features", + "Authors@R": "c( person(\"Lionel\", \"Henry\", , \"lionel@posit.co\", role = c(\"aut\", \"cre\")), person(\"Hadley\", \"Wickham\", , \"hadley@posit.co\", role = \"aut\"), person(\"mikefc\", , , \"mikefc@coolbutuseless.com\", role = \"cph\", comment = \"Hash implementation based on Mike's xxhashlite\"), person(\"Yann\", \"Collet\", role = \"cph\", comment = \"Author of the embedded xxHash library\"), person(\"Posit, PBC\", role = c(\"cph\", \"fnd\")) )", + "Description": "A toolbox for working with base types, core R features like the condition system, and core 'Tidyverse' features like tidy evaluation.", + "License": "MIT + file LICENSE", + "URL": "https://rlang.r-lib.org, https://github.com/r-lib/rlang", + "BugReports": "https://github.com/r-lib/rlang/issues", + "Depends": [ + "R (>= 4.0.0)" + ], + "Imports": [ + "utils" + ], + "Suggests": [ + "cli (>= 3.1.0)", + "covr", + "crayon", + "desc", + "fs", + "glue", + "knitr", + "magrittr", + "methods", + "pillar", + "pkgload", + "rmarkdown", + "stats", + "testthat (>= 3.3.2)", + "tibble", + "usethis", + "vctrs (>= 0.2.3)", + "withr" + ], + "Enhances": [ + "winch" + ], + "Biarch": "true", + "ByteCompile": "true", + "Config/build/compilation-database": "true", + "Config/Needs/website": "dplyr, tidyverse/tidytemplate", + "Config/roxygen2/version": "8.0.0", + "Config/testthat/edition": "3", + "Config/testthat/parallel": "true", + "Encoding": "UTF-8", + "NeedsCompilation": "yes", + "Author": "Lionel Henry [aut, cre], Hadley Wickham [aut], mikefc [cph] (Hash implementation based on Mike's xxhashlite), Yann Collet [cph] (Author of the embedded xxHash library), Posit, PBC [cph, fnd]", + "Maintainer": "Lionel Henry ", + "Repository": "CRAN" + }, + "rpart": { + "Package": "rpart", + "Version": "4.1.27", + "Source": "Repository", + "Priority": "recommended", + "Date": "2026-03-26", + "Authors@R": "c(person(\"Terry\", \"Therneau\", role = \"aut\", email = \"therneau@mayo.edu\"), person(\"Beth\", \"Atkinson\", role = c(\"aut\", \"cre\"), email = \"atkinson@mayo.edu\"), person(\"Brian\", \"Ripley\", role = \"trl\", email = \"Brian.Ripley@R-project.org\", comment = \"producer of the initial R port, maintainer 1999-2017\"))", + "Description": "Recursive partitioning for classification, regression and survival trees. An implementation of most of the functionality of the 1984 book by Breiman, Friedman, Olshen and Stone.", + "Title": "Recursive Partitioning and Regression Trees", + "Depends": [ + "R (>= 2.15.0)", + "graphics", + "stats", + "grDevices" + ], + "Suggests": [ + "survival" + ], + "License": "GPL-2 | GPL-3", + "LazyData": "yes", + "ByteCompile": "yes", + "NeedsCompilation": "yes", + "Author": "Terry Therneau [aut], Beth Atkinson [aut, cre], Brian Ripley [trl] (producer of the initial R port, maintainer 1999-2017)", + "Maintainer": "Beth Atkinson ", + "Repository": "CRAN", + "URL": "https://github.com/bethatkinson/rpart, https://cran.r-project.org/package=rpart", + "BugReports": "https://github.com/bethatkinson/rpart/issues" + }, + "sass": { + "Package": "sass", + "Version": "0.4.10", + "Source": "Repository", + "Type": "Package", + "Title": "Syntactically Awesome Style Sheets ('Sass')", + "Description": "An 'SCSS' compiler, powered by the 'LibSass' library. With this, R developers can use variables, inheritance, and functions to generate dynamic style sheets. The package uses the 'Sass CSS' extension language, which is stable, powerful, and CSS compatible.", + "Authors@R": "c( person(\"Joe\", \"Cheng\", , \"joe@rstudio.com\", \"aut\"), person(\"Timothy\", \"Mastny\", , \"tim.mastny@gmail.com\", \"aut\"), person(\"Richard\", \"Iannone\", , \"rich@rstudio.com\", \"aut\", comment = c(ORCID = \"0000-0003-3925-190X\")), person(\"Barret\", \"Schloerke\", , \"barret@rstudio.com\", \"aut\", comment = c(ORCID = \"0000-0001-9986-114X\")), person(\"Carson\", \"Sievert\", , \"carson@rstudio.com\", c(\"aut\", \"cre\"), comment = c(ORCID = \"0000-0002-4958-2844\")), person(\"Christophe\", \"Dervieux\", , \"cderv@rstudio.com\", c(\"ctb\"), comment = c(ORCID = \"0000-0003-4474-2498\")), person(family = \"RStudio\", role = c(\"cph\", \"fnd\")), person(family = \"Sass Open Source Foundation\", role = c(\"ctb\", \"cph\"), comment = \"LibSass library\"), person(\"Greter\", \"Marcel\", role = c(\"ctb\", \"cph\"), comment = \"LibSass library\"), person(\"Mifsud\", \"Michael\", role = c(\"ctb\", \"cph\"), comment = \"LibSass library\"), person(\"Hampton\", \"Catlin\", role = c(\"ctb\", \"cph\"), comment = \"LibSass library\"), person(\"Natalie\", \"Weizenbaum\", role = c(\"ctb\", \"cph\"), comment = \"LibSass library\"), person(\"Chris\", \"Eppstein\", role = c(\"ctb\", \"cph\"), comment = \"LibSass library\"), person(\"Adams\", \"Joseph\", role = c(\"ctb\", \"cph\"), comment = \"json.cpp\"), person(\"Trifunovic\", \"Nemanja\", role = c(\"ctb\", \"cph\"), comment = \"utf8.h\") )", + "License": "MIT + file LICENSE", + "URL": "https://rstudio.github.io/sass/, https://github.com/rstudio/sass", + "BugReports": "https://github.com/rstudio/sass/issues", + "Encoding": "UTF-8", + "RoxygenNote": "7.3.2", + "SystemRequirements": "GNU make", + "Imports": [ + "fs (>= 1.2.4)", + "rlang (>= 0.4.10)", + "htmltools (>= 0.5.1)", + "R6", + "rappdirs" + ], + "Suggests": [ + "testthat", + "knitr", + "rmarkdown", + "withr", + "shiny", + "curl" + ], + "VignetteBuilder": "knitr", + "Config/testthat/edition": "3", + "NeedsCompilation": "yes", + "Author": "Joe Cheng [aut], Timothy Mastny [aut], Richard Iannone [aut] (), Barret Schloerke [aut] (), Carson Sievert [aut, cre] (), Christophe Dervieux [ctb] (), RStudio [cph, fnd], Sass Open Source Foundation [ctb, cph] (LibSass library), Greter Marcel [ctb, cph] (LibSass library), Mifsud Michael [ctb, cph] (LibSass library), Hampton Catlin [ctb, cph] (LibSass library), Natalie Weizenbaum [ctb, cph] (LibSass library), Chris Eppstein [ctb, cph] (LibSass library), Adams Joseph [ctb, cph] (json.cpp), Trifunovic Nemanja [ctb, cph] (utf8.h)", + "Maintainer": "Carson Sievert ", + "Repository": "CRAN" + }, + "shiny": { + "Package": "shiny", + "Version": "1.14.0", + "Source": "Repository", + "Type": "Package", + "Title": "Web Application Framework for R", + "Authors@R": "c( person(\"Winston\", \"Chang\", , \"winston@posit.co\", role = \"aut\", comment = c(ORCID = \"0000-0002-1576-2126\")), person(\"Joe\", \"Cheng\", , \"joe@posit.co\", role = \"aut\"), person(\"JJ\", \"Allaire\", , \"jj@posit.co\", role = \"aut\"), person(\"Carson\", \"Sievert\", , \"carson@posit.co\", role = c(\"aut\", \"cre\"), comment = c(ORCID = \"0000-0002-4958-2844\")), person(\"Barret\", \"Schloerke\", , \"barret@posit.co\", role = \"aut\", comment = c(ORCID = \"0000-0001-9986-114X\")), person(\"Garrick\", \"Aden-Buie\", , \"garrick@adenbuie.com\", role = \"aut\", comment = c(ORCID = \"0000-0002-7111-0077\")), person(\"Yihui\", \"Xie\", , \"yihui@posit.co\", role = \"aut\"), person(\"Jeff\", \"Allen\", role = \"aut\"), person(\"Jonathan\", \"McPherson\", , \"jonathan@posit.co\", role = \"aut\"), person(\"Alan\", \"Dipert\", role = \"aut\"), person(\"Barbara\", \"Borges\", role = \"aut\"), person(\"Posit Software, PBC\", role = c(\"cph\", \"fnd\"), comment = c(ROR = \"03wc8by49\")), person(, \"jQuery Foundation\", role = \"cph\", comment = \"jQuery library and jQuery UI library\"), person(, \"jQuery contributors\", role = c(\"ctb\", \"cph\"), comment = \"jQuery library; authors listed in inst/www/shared/jquery-AUTHORS.txt\"), person(, \"jQuery UI contributors\", role = c(\"ctb\", \"cph\"), comment = \"jQuery UI library; authors listed in inst/www/shared/jqueryui/AUTHORS.txt\"), person(\"Mark\", \"Otto\", role = \"ctb\", comment = \"Bootstrap library\"), person(\"Jacob\", \"Thornton\", role = \"ctb\", comment = \"Bootstrap library\"), person(, \"Bootstrap contributors\", role = \"ctb\", comment = \"Bootstrap library\"), person(, \"Twitter, Inc\", role = \"cph\", comment = \"Bootstrap library\"), person(\"Prem Nawaz\", \"Khan\", role = \"ctb\", comment = \"Bootstrap accessibility plugin\"), person(\"Victor\", \"Tsaran\", role = \"ctb\", comment = \"Bootstrap accessibility plugin\"), person(\"Dennis\", \"Lembree\", role = \"ctb\", comment = \"Bootstrap accessibility plugin\"), person(\"Srinivasu\", \"Chakravarthula\", role = \"ctb\", comment = \"Bootstrap accessibility plugin\"), person(\"Cathy\", \"O'Connor\", role = \"ctb\", comment = \"Bootstrap accessibility plugin\"), person(, \"PayPal, Inc\", role = \"cph\", comment = \"Bootstrap accessibility plugin\"), person(\"Stefan\", \"Petre\", role = c(\"ctb\", \"cph\"), comment = \"Bootstrap-datepicker library\"), person(\"Andrew\", \"Rowls\", role = c(\"ctb\", \"cph\"), comment = \"Bootstrap-datepicker library\"), person(\"Brian\", \"Reavis\", role = c(\"ctb\", \"cph\"), comment = \"selectize.js library\"), person(\"Salmen\", \"Bejaoui\", role = c(\"ctb\", \"cph\"), comment = \"selectize-plugin-a11y library\"), person(\"Denis\", \"Ineshin\", role = c(\"ctb\", \"cph\"), comment = \"ion.rangeSlider library\"), person(\"Sami\", \"Samhuri\", role = c(\"ctb\", \"cph\"), comment = \"Javascript strftime library\"), person(, \"SpryMedia Limited\", role = c(\"ctb\", \"cph\"), comment = \"DataTables library\"), person(\"Ivan\", \"Sagalaev\", role = c(\"ctb\", \"cph\"), comment = \"highlight.js library\"), person(\"R Core Team\", role = c(\"ctb\", \"cph\"), comment = \"tar implementation from R\") )", + "Description": "Makes it incredibly easy to build interactive web applications with R. Automatic \"reactive\" binding between inputs and outputs and extensive prebuilt widgets make it possible to build beautiful, responsive, and powerful applications with minimal effort.", + "License": "MIT + file LICENSE", + "URL": "https://shiny.posit.co/, https://github.com/rstudio/shiny", + "BugReports": "https://github.com/rstudio/shiny/issues", + "Depends": [ + "methods", + "R (>= 3.1.2)" + ], + "Imports": [ + "bslib (>= 0.6.0)", + "cachem (>= 1.1.0)", + "cli", + "commonmark (>= 2.0.0)", + "fastmap (>= 1.1.1)", + "fontawesome (>= 0.4.0)", + "glue (>= 1.3.2)", + "grDevices", + "htmltools (>= 0.5.4)", + "httpuv (>= 1.5.2)", + "jsonlite (>= 0.9.16)", + "later (>= 1.0.0)", + "lifecycle (>= 0.2.0)", + "mime (>= 0.3)", + "otel", + "promises (>= 1.5.0)", + "R6 (>= 2.0)", + "rlang (>= 0.4.10)", + "sourcetools", + "tools", + "utils", + "withr", + "xtable" + ], + "Suggests": [ + "Cairo (>= 1.5-5)", + "coro (>= 1.1.0)", + "datasets", + "DT", + "dygraphs", + "future", + "ggplot2", + "knitr (>= 1.6)", + "magrittr", + "markdown", + "mirai", + "otelsdk (>= 0.2.0)", + "ragg", + "reactlog (>= 1.0.0)", + "rmarkdown", + "sass", + "shinytest2", + "showtext", + "testthat (>= 3.2.1)", + "watcher", + "yaml" + ], + "Config/Needs/check": "shinyjs", + "Config/roxygen2/version": "8.0.0", + "Config/testthat/edition": "3", + "Encoding": "UTF-8", + "Collate": "'app-handle.R' 'globals.R' 'app-state.R' 'app_template.R' 'bind-cache.R' 'bind-event.R' 'bookmark-state-local.R' 'bookmark-state.R' 'bootstrap-deprecated.R' 'bootstrap-layout.R' 'conditions.R' 'map.R' 'utils.R' 'bootstrap.R' 'busy-indicators-spinners.R' 'busy-indicators.R' 'cache-utils.R' 'deprecated.R' 'devmode.R' 'diagnose.R' 'extended-task.R' 'fileupload.R' 'graph.R' 'reactives.R' 'reactive-domains.R' 'history.R' 'hooks.R' 'html-deps.R' 'image-interact-opts.R' 'image-interact.R' 'imageutils.R' 'input-action.R' 'input-checkbox.R' 'input-checkboxgroup.R' 'input-date.R' 'input-daterange.R' 'input-file.R' 'input-numeric.R' 'input-password.R' 'input-radiobuttons.R' 'input-select.R' 'input-slider.R' 'input-submit.R' 'input-text.R' 'input-textarea.R' 'input-utils.R' 'insert-tab.R' 'insert-ui.R' 'jqueryui.R' 'knitr.R' 'middleware-shiny.R' 'middleware.R' 'timer.R' 'shiny.R' 'mock-session.R' 'modal.R' 'modules.R' 'notifications.R' 'otel-attr-srcref.R' 'otel-collect.R' 'otel-enable.R' 'otel-error.R' 'otel-label.R' 'otel-reactive-update.R' 'otel-session.R' 'otel-shiny.R' 'otel-with.R' 'priorityqueue.R' 'progress.R' 'react.R' 'reexports.R' 'render-cached-plot.R' 'render-plot.R' 'render-table.R' 'run-url.R' 'runapp.R' 'serializers.R' 'server-input-handlers.R' 'server-resource-paths.R' 'server.R' 'shiny-options.R' 'shiny-package.R' 'shinyapp.R' 'shinyui.R' 'shinywrappers.R' 'showcase.R' 'snapshot.R' 'staticimports.R' 'tar.R' 'test-export.R' 'test-server.R' 'test.R' 'update-input.R' 'utils-lang.R' 'utils-tags.R' 'version_bs_date_picker.R' 'version_ion_range_slider.R' 'version_jquery.R' 'version_jqueryui.R' 'version_selectize.R' 'version_strftime.R' 'viewer.R'", + "NeedsCompilation": "no", + "Author": "Winston Chang [aut] (ORCID: ), Joe Cheng [aut], JJ Allaire [aut], Carson Sievert [aut, cre] (ORCID: ), Barret Schloerke [aut] (ORCID: ), Garrick Aden-Buie [aut] (ORCID: ), Yihui Xie [aut], Jeff Allen [aut], Jonathan McPherson [aut], Alan Dipert [aut], Barbara Borges [aut], Posit Software, PBC [cph, fnd] (ROR: ), jQuery Foundation [cph] (jQuery library and jQuery UI library), jQuery contributors [ctb, cph] (jQuery library; authors listed in inst/www/shared/jquery-AUTHORS.txt), jQuery UI contributors [ctb, cph] (jQuery UI library; authors listed in inst/www/shared/jqueryui/AUTHORS.txt), Mark Otto [ctb] (Bootstrap library), Jacob Thornton [ctb] (Bootstrap library), Bootstrap contributors [ctb] (Bootstrap library), Twitter, Inc [cph] (Bootstrap library), Prem Nawaz Khan [ctb] (Bootstrap accessibility plugin), Victor Tsaran [ctb] (Bootstrap accessibility plugin), Dennis Lembree [ctb] (Bootstrap accessibility plugin), Srinivasu Chakravarthula [ctb] (Bootstrap accessibility plugin), Cathy O'Connor [ctb] (Bootstrap accessibility plugin), PayPal, Inc [cph] (Bootstrap accessibility plugin), Stefan Petre [ctb, cph] (Bootstrap-datepicker library), Andrew Rowls [ctb, cph] (Bootstrap-datepicker library), Brian Reavis [ctb, cph] (selectize.js library), Salmen Bejaoui [ctb, cph] (selectize-plugin-a11y library), Denis Ineshin [ctb, cph] (ion.rangeSlider library), Sami Samhuri [ctb, cph] (Javascript strftime library), SpryMedia Limited [ctb, cph] (DataTables library), Ivan Sagalaev [ctb, cph] (highlight.js library), R Core Team [ctb, cph] (tar implementation from R)", + "Maintainer": "Carson Sievert ", + "Repository": "CRAN" + }, + "sourcetools": { + "Package": "sourcetools", + "Version": "0.1.7-2", + "Source": "Repository", + "Type": "Package", + "Title": "Tools for Reading, Tokenizing and Parsing R Code", + "Authors@R": "person(\"Kevin\", \"Ushey\", role = c(\"aut\", \"cre\"), email = \"kevinushey@gmail.com\")", + "Maintainer": "Kevin Ushey ", + "Description": "Tools for the reading and tokenization of R code. The 'sourcetools' package provides both an R and C++ interface for the tokenization of R code, and helpers for interacting with the tokenized representation of R code.", + "License": "MIT + file LICENSE", + "Depends": [ + "R (>= 3.0.2)" + ], + "Suggests": [ + "testthat" + ], + "RoxygenNote": "5.0.1", + "BugReports": "https://github.com/kevinushey/sourcetools/issues", + "Encoding": "UTF-8", + "NeedsCompilation": "yes", + "Author": "Kevin Ushey [aut, cre]", + "Repository": "CRAN" + }, + "spatial": { + "Package": "spatial", + "Version": "7.3-18", + "Source": "Repository", + "Priority": "recommended", + "Date": "2025-01-01", + "Depends": [ + "R (>= 3.0.0)", + "graphics", + "stats", + "utils" + ], + "Suggests": [ + "MASS" + ], + "Authors@R": "c(person(\"Brian\", \"Ripley\", role = c(\"aut\", \"cre\", \"cph\"), email = \"Brian.Ripley@R-project.org\"), person(\"Roger\", \"Bivand\", role = \"ctb\"), person(\"William\", \"Venables\", role = \"cph\"))", + "Description": "Functions for kriging and point pattern analysis.", + "Title": "Functions for Kriging and Point Pattern Analysis", + "LazyLoad": "yes", + "ByteCompile": "yes", + "License": "GPL-2 | GPL-3", + "URL": "http://www.stats.ox.ac.uk/pub/MASS4/", + "NeedsCompilation": "yes", + "Author": "Brian Ripley [aut, cre, cph], Roger Bivand [ctb], William Venables [cph]", + "Maintainer": "Brian Ripley ", + "Repository": "CRAN" + }, + "survival": { + "Package": "survival", + "Version": "3.8-6", + "Source": "Repository", + "Title": "Survival Analysis", + "Priority": "recommended", + "Date": "2026-01-09", + "Depends": [ + "R (>= 3.5.0)" + ], + "Imports": [ + "graphics", + "Matrix", + "methods", + "splines", + "stats", + "utils" + ], + "LazyData": "Yes", + "LazyDataCompression": "xz", + "ByteCompile": "Yes", + "Authors@R": "c(person(c(\"Terry\", \"M\"), \"Therneau\", email=\"therneau.terry@mayo.edu\", role=c(\"aut\", \"cre\")), person(\"Thomas\", \"Lumley\", role=c(\"ctb\", \"trl\"), comment=\"original S->R port and R maintainer until 2009\"), person(\"Atkinson\", \"Elizabeth\", role=\"ctb\"), person(\"Crowson\", \"Cynthia\", role=\"ctb\"))", + "Description": "Contains the core survival analysis routines, including definition of Surv objects, Kaplan-Meier and Aalen-Johansen (multi-state) curves, Cox models, and parametric accelerated failure time models.", + "License": "LGPL (>= 2)", + "URL": "https://github.com/therneau/survival", + "NeedsCompilation": "yes", + "Author": "Terry M Therneau [aut, cre], Thomas Lumley [ctb, trl] (original S->R port and R maintainer until 2009), Atkinson Elizabeth [ctb], Crowson Cynthia [ctb]", + "Maintainer": "Terry M Therneau ", + "Repository": "CRAN" + }, + "timechange": { + "Package": "timechange", + "Version": "0.4.0", + "Source": "Repository", + "Title": "Efficient Manipulation of Date-Times", + "Authors@R": "c(person(\"Vitalie\", \"Spinu\", email = \"spinuvit@gmail.com\", role = c(\"aut\", \"cre\")), person(\"Google Inc.\", role = c(\"ctb\", \"cph\")))", + "Description": "Efficient routines for manipulation of date-time objects while accounting for time-zones and daylight saving times. The package includes utilities for updating of date-time components (year, month, day etc.), modification of time-zones, rounding of date-times, period addition and subtraction etc. Parts of the 'CCTZ' source code, released under the Apache 2.0 License, are included in this package. See for more details.", + "Depends": [ + "R (>= 3.3)" + ], + "License": "GPL (>= 3)", + "Encoding": "UTF-8", + "LinkingTo": [ + "cpp11 (>= 0.2.7)" + ], + "Suggests": [ + "testthat (>= 0.7.1.99)", + "knitr" + ], + "SystemRequirements": "A system with zoneinfo data (e.g. /usr/share/zoneinfo). On Windows the zoneinfo included with R is used.", + "BugReports": "https://github.com/vspinu/timechange/issues", + "URL": "https://github.com/vspinu/timechange/", + "RoxygenNote": "7.2.1", + "NeedsCompilation": "yes", + "Author": "Vitalie Spinu [aut, cre], Google Inc. [ctb, cph]", + "Maintainer": "Vitalie Spinu ", + "Repository": "CRAN" + }, + "vctrs": { + "Package": "vctrs", + "Version": "0.7.3", + "Source": "Repository", + "Title": "Vector Helpers", + "Authors@R": "c( person(\"Hadley\", \"Wickham\", , \"hadley@posit.co\", role = \"aut\"), person(\"Lionel\", \"Henry\", , \"lionel@posit.co\", role = \"aut\"), person(\"Davis\", \"Vaughan\", , \"davis@posit.co\", role = c(\"aut\", \"cre\")), person(\"data.table team\", role = \"cph\", comment = \"Radix sort based on data.table's forder() and their contribution to R's order()\"), person(\"Posit Software, PBC\", role = c(\"cph\", \"fnd\")) )", + "Description": "Defines new notions of prototype and size that are used to provide tools for consistent and well-founded type-coercion and size-recycling, and are in turn connected to ideas of type- and size-stability useful for analysing function interfaces.", + "License": "MIT + file LICENSE", + "URL": "https://vctrs.r-lib.org/, https://github.com/r-lib/vctrs", + "BugReports": "https://github.com/r-lib/vctrs/issues", + "Depends": [ + "R (>= 4.0.0)" + ], + "Imports": [ + "cli (>= 3.4.0)", + "glue", + "lifecycle (>= 1.0.3)", + "rlang (>= 1.1.7)" + ], + "Suggests": [ + "bit64", + "covr", + "crayon", + "dplyr (>= 0.8.5)", + "generics", + "knitr", + "pillar (>= 1.4.4)", + "pkgdown (>= 2.0.1)", + "rmarkdown", + "testthat (>= 3.0.0)", + "tibble (>= 3.1.3)", + "waldo (>= 0.2.0)", + "withr", + "xml2", + "zeallot" + ], + "VignetteBuilder": "knitr", + "Config/build/compilation-database": "true", + "Config/Needs/website": "tidyverse/tidytemplate", + "Config/testthat/edition": "3", + "Config/testthat/parallel": "true", + "Encoding": "UTF-8", + "KeepSource": "true", + "Language": "en-GB", + "RoxygenNote": "7.3.3", + "NeedsCompilation": "yes", + "Author": "Hadley Wickham [aut], Lionel Henry [aut], Davis Vaughan [aut, cre], data.table team [cph] (Radix sort based on data.table's forder() and their contribution to R's order()), Posit Software, PBC [cph, fnd]", + "Maintainer": "Davis Vaughan ", + "Repository": "CRAN" + }, + "withr": { + "Package": "withr", + "Version": "3.0.3", + "Source": "Repository", + "Title": "Run Code 'With' Temporarily Modified Global State", + "Authors@R": "c( person(\"Jim\", \"Hester\", role = \"aut\"), person(\"Lionel\", \"Henry\", , \"lionel@posit.co\", role = c(\"aut\", \"cre\")), person(\"Kirill\", \"Müller\", , \"krlmlr+r@mailbox.org\", role = \"aut\"), person(\"Kevin\", \"Ushey\", , \"kevinushey@gmail.com\", role = \"aut\"), person(\"Hadley\", \"Wickham\", , \"hadley@posit.co\", role = \"aut\"), person(\"Winston\", \"Chang\", role = \"aut\"), person(\"Jennifer\", \"Bryan\", role = \"ctb\"), person(\"Richard\", \"Cotton\", role = \"ctb\"), person(\"Posit Software, PBC\", role = c(\"cph\", \"fnd\")) )", + "Description": "A set of functions to run code 'with' safely and temporarily modified global state. Many of these functions were originally a part of the 'devtools' package, this provides a simple package with limited dependencies to provide access to these functions.", + "License": "MIT + file LICENSE", + "URL": "https://withr.r-lib.org, https://github.com/r-lib/withr#readme", + "BugReports": "https://github.com/r-lib/withr/issues", + "Depends": [ + "R (>= 3.6.0)" + ], + "Imports": [ + "graphics", + "grDevices" + ], + "Suggests": [ + "callr", + "DBI", + "knitr", + "methods", + "rlang", + "rmarkdown (>= 2.12)", + "RSQLite", + "testthat (>= 3.0.0)" + ], + "VignetteBuilder": "knitr", + "Config/Needs/website": "tidyverse/tidytemplate", + "Config/testthat/edition": "3", + "Encoding": "UTF-8", + "RoxygenNote": "7.3.2", + "Collate": "'aaa.R' 'collate.R' 'connection.R' 'db.R' 'defer-exit.R' 'standalone-defer.R' 'defer.R' 'devices.R' 'local_.R' 'with_.R' 'dir.R' 'env.R' 'file.R' 'language.R' 'libpaths.R' 'locale.R' 'makevars.R' 'namespace.R' 'options.R' 'par.R' 'path.R' 'rng.R' 'seed.R' 'wrap.R' 'sink.R' 'tempfile.R' 'timezone.R' 'torture.R' 'utils.R' 'with.R'", + "NeedsCompilation": "no", + "Author": "Jim Hester [aut], Lionel Henry [aut, cre], Kirill Müller [aut], Kevin Ushey [aut], Hadley Wickham [aut], Winston Chang [aut], Jennifer Bryan [ctb], Richard Cotton [ctb], Posit Software, PBC [cph, fnd]", + "Maintainer": "Lionel Henry ", + "Repository": "CRAN" + }, + "xtable": { + "Package": "xtable", + "Version": "1.8-8", + "Source": "Repository", + "Date": "2026-02-20", + "Title": "Export Tables to LaTeX or HTML", + "Authors@R": "c(person(\"David B.\", \"Dahl\", role=\"aut\"), person(\"David\", \"Scott\", role=c(\"aut\",\"cre\"), email=\"d.scott@auckland.ac.nz\"), person(\"Charles\", \"Roosen\", role=\"aut\"), person(\"Arni\", \"Magnusson\", role=\"aut\"), person(\"Jonathan\", \"Swinton\", role=\"aut\"), person(\"Ajay\", \"Shah\", role=\"ctb\"), person(\"Arne\", \"Henningsen\", role=\"ctb\"), person(\"Benno\", \"Puetz\", role=\"ctb\"), person(\"Bernhard\", \"Pfaff\", role=\"ctb\"), person(\"Claudio\", \"Agostinelli\", role=\"ctb\"), person(\"Claudius\", \"Loehnert\", role=\"ctb\"), person(\"David\", \"Mitchell\", role=\"ctb\"), person(\"David\", \"Whiting\", role=\"ctb\"), person(\"Fernando da\", \"Rosa\", role=\"ctb\"), person(\"Guido\", \"Gay\", role=\"ctb\"), person(\"Guido\", \"Schulz\", role=\"ctb\"), person(\"Ian\", \"Fellows\", role=\"ctb\"), person(\"Jeff\", \"Laake\", role=\"ctb\"), person(\"John\", \"Walker\", role=\"ctb\"), person(\"Jun\", \"Yan\", role=\"ctb\"), person(\"Liviu\", \"Andronic\", role=\"ctb\"), person(\"Markus\", \"Loecher\", role=\"ctb\"), person(\"Martin\", \"Gubri\", role=\"ctb\"), person(\"Matthieu\", \"Stigler\", role=\"ctb\"), person(\"Robert\", \"Castelo\", role=\"ctb\"), person(\"Seth\", \"Falcon\", role=\"ctb\"), person(\"Stefan\", \"Edwards\", role=\"ctb\"), person(\"Sven\", \"Garbade\", role=\"ctb\"), person(\"Uwe\", \"Ligges\", role=\"ctb\"))", + "Maintainer": "David Scott ", + "Imports": [ + "stats", + "utils", + "methods" + ], + "Suggests": [ + "knitr", + "zoo", + "survival", + "glue", + "tinytex" + ], + "VignetteBuilder": "knitr", + "Description": "Coerce data to LaTeX and HTML tables.", + "URL": "http://xtable.r-forge.r-project.org/", + "Depends": [ + "R (>= 2.10.0)" + ], + "License": "GPL (>= 2)", + "Repository": "CRAN", + "NeedsCompilation": "no", + "Author": "David B. Dahl [aut], David Scott [aut, cre], Charles Roosen [aut], Arni Magnusson [aut], Jonathan Swinton [aut], Ajay Shah [ctb], Arne Henningsen [ctb], Benno Puetz [ctb], Bernhard Pfaff [ctb], Claudio Agostinelli [ctb], Claudius Loehnert [ctb], David Mitchell [ctb], David Whiting [ctb], Fernando da Rosa [ctb], Guido Gay [ctb], Guido Schulz [ctb], Ian Fellows [ctb], Jeff Laake [ctb], John Walker [ctb], Jun Yan [ctb], Liviu Andronic [ctb], Markus Loecher [ctb], Martin Gubri [ctb], Matthieu Stigler [ctb], Robert Castelo [ctb], Seth Falcon [ctb], Stefan Edwards [ctb], Sven Garbade [ctb], Uwe Ligges [ctb]" + } + } +} diff --git a/applications/poll-studio/renv/.gitignore b/applications/poll-studio/renv/.gitignore new file mode 100644 index 00000000..0ec0cbba --- /dev/null +++ b/applications/poll-studio/renv/.gitignore @@ -0,0 +1,7 @@ +library/ +local/ +cellar/ +lock/ +python/ +sandbox/ +staging/ diff --git a/applications/poll-studio/renv/activate.R b/applications/poll-studio/renv/activate.R new file mode 100644 index 00000000..15a85bbe --- /dev/null +++ b/applications/poll-studio/renv/activate.R @@ -0,0 +1,1451 @@ + +local({ + + # the requested version of renv + version <- "1.3.0" + attr(version, "md5") <- "47c9e9df3aa6bd1c2f0be992b05279e2" + attr(version, "sha") <- NULL + + # the project directory + project <- Sys.getenv("RENV_PROJECT") + if (!nzchar(project)) + project <- getwd() + + # use start-up diagnostics if enabled + diagnostics <- Sys.getenv("RENV_STARTUP_DIAGNOSTICS", unset = "FALSE") + if (diagnostics) { + start <- Sys.time() + profile <- tempfile("renv-startup-", fileext = ".Rprof") + utils::Rprof(profile) + on.exit({ + utils::Rprof(NULL) + elapsed <- signif(difftime(Sys.time(), start, units = "auto"), digits = 2L) + writeLines(sprintf("- renv took %s to run the autoloader.", format(elapsed))) + writeLines(sprintf("- Profile: %s", profile)) + print(utils::summaryRprof(profile)) + }, add = TRUE) + } + + # figure out whether the autoloader is enabled + enabled <- local({ + + # first, check config option + override <- getOption("renv.config.autoloader.enabled") + if (!is.null(override)) + return(override) + + # if we're being run in a context where R_LIBS is already set, + # don't load -- presumably we're being run as a sub-process and + # the parent process has already set up library paths for us + rcmd <- Sys.getenv("R_CMD", unset = NA) + rlibs <- Sys.getenv("R_LIBS", unset = NA) + if (!is.na(rlibs) && !is.na(rcmd)) + return(FALSE) + + # next, check environment variables + # prefer using the configuration one in the future + envvars <- c( + "RENV_CONFIG_AUTOLOADER_ENABLED", + "RENV_AUTOLOADER_ENABLED", + "RENV_ACTIVATE_PROJECT" + ) + + for (envvar in envvars) { + envval <- Sys.getenv(envvar, unset = NA) + if (!is.na(envval)) + return(tolower(envval) %in% c("true", "t", "1")) + } + + # enable by default + TRUE + + }) + + # bail if we're not enabled + if (!enabled) { + + # if we're not enabled, we might still need to manually load + # the user profile here + profile <- Sys.getenv("R_PROFILE_USER", unset = "~/.Rprofile") + if (file.exists(profile)) { + cfg <- Sys.getenv("RENV_CONFIG_USER_PROFILE", unset = "TRUE") + if (tolower(cfg) %in% c("true", "t", "1")) + sys.source(profile, envir = globalenv()) + } + + return(FALSE) + + } + + # avoid recursion + if (identical(getOption("renv.autoloader.running"), TRUE)) { + warning("ignoring recursive attempt to run renv autoloader") + return(invisible(TRUE)) + } + + # signal that we're loading renv during R startup + options(renv.autoloader.running = TRUE) + on.exit(options(renv.autoloader.running = NULL), add = TRUE) + + # signal that we've consented to use renv + options(renv.consent = TRUE) + + # load the 'utils' package eagerly -- this ensures that renv shims, which + # mask 'utils' packages, will come first on the search path + library(utils, lib.loc = .Library) + + # unload renv if it's already been loaded + if ("renv" %in% loadedNamespaces()) + unloadNamespace("renv") + + # load bootstrap tools + ansify <- function(text) { + if (renv_ansify_enabled()) + renv_ansify_enhanced(text) + else + renv_ansify_default(text) + } + + renv_ansify_enabled <- function() { + + override <- Sys.getenv("RENV_ANSIFY_ENABLED", unset = NA) + if (!is.na(override)) + return(as.logical(override)) + + pane <- Sys.getenv("RSTUDIO_CHILD_PROCESS_PANE", unset = NA) + if (identical(pane, "build")) + return(FALSE) + + testthat <- Sys.getenv("TESTTHAT", unset = "false") + if (tolower(testthat) %in% "true") + return(FALSE) + + iderun <- Sys.getenv("R_CLI_HAS_HYPERLINK_IDE_RUN", unset = "false") + if (tolower(iderun) %in% "false") + return(FALSE) + + TRUE + + } + + renv_ansify_default <- function(text) { + text + } + + renv_ansify_enhanced <- function(text) { + + # R help links + pattern <- "`\\?(renv::(?:[^`])+)`" + replacement <- "`\033]8;;x-r-help:\\1\a?\\1\033]8;;\a`" + text <- gsub(pattern, replacement, text, perl = TRUE) + + # runnable code + pattern <- "`(renv::(?:[^`])+)`" + replacement <- "`\033]8;;x-r-run:\\1\a\\1\033]8;;\a`" + text <- gsub(pattern, replacement, text, perl = TRUE) + + # return ansified text + text + + } + + renv_ansify_init <- function() { + + envir <- renv_envir_self() + if (renv_ansify_enabled()) + assign("ansify", renv_ansify_enhanced, envir = envir) + else + assign("ansify", renv_ansify_default, envir = envir) + + } + + `%||%` <- function(x, y) { + if (is.null(x)) y else x + } + + catf <- function(fmt, ..., appendLF = TRUE) { + + quiet <- getOption("renv.bootstrap.quiet", default = FALSE) + if (quiet) + return(invisible()) + + # also check for config environment variables that should suppress messages + # https://github.com/rstudio/renv/issues/2214 + enabled <- Sys.getenv("RENV_CONFIG_STARTUP_QUIET", unset = NA) + if (!is.na(enabled) && tolower(enabled) %in% c("true", "1")) + return(invisible()) + + enabled <- Sys.getenv("RENV_CONFIG_SYNCHRONIZED_CHECK", unset = NA) + if (!is.na(enabled) && tolower(enabled) %in% c("false", "0")) + return(invisible()) + + msg <- sprintf(fmt, ...) + cat(msg, file = stdout(), sep = if (appendLF) "\n" else "") + + invisible(msg) + + } + + header <- function(label, + ..., + prefix = "#", + suffix = "-", + n = min(getOption("width"), 78)) + { + label <- sprintf(label, ...) + n <- max(n - nchar(label) - nchar(prefix) - 2L, 8L) + if (n <= 0) + return(paste(prefix, label)) + + tail <- paste(rep.int(suffix, n), collapse = "") + paste0(prefix, " ", label, " ", tail) + + } + + heredoc <- function(text, leave = 0) { + + # remove leading, trailing whitespace + trimmed <- gsub("^\\s*\\n|\\n\\s*$", "", text) + + # split into lines + lines <- strsplit(trimmed, "\n", fixed = TRUE)[[1L]] + + # compute common indent + indent <- regexpr("[^[:space:]]", lines) + common <- min(setdiff(indent, -1L)) - leave + text <- paste(substring(lines, common), collapse = "\n") + + # substitute in ANSI links for executable renv code + ansify(text) + + } + + bootstrap <- function(version, library) { + + friendly <- renv_bootstrap_version_friendly(version) + section <- header(sprintf("Bootstrapping renv %s", friendly)) + catf(section) + + # ensure the target library path exists; required for file.copy(..., recursive = TRUE) + dir.create(library, showWarnings = FALSE, recursive = TRUE) + + # try to install renv from cache + md5 <- attr(version, "md5", exact = TRUE) + if (length(md5)) { + pkgpath <- renv_bootstrap_find(version) + if (length(pkgpath) && file.exists(pkgpath)) { + ok <- file.copy(pkgpath, library, recursive = TRUE) + if (isTRUE(ok)) + return(invisible()) + } + } + + # attempt to download renv + catf("- Downloading renv ... ", appendLF = FALSE) + withCallingHandlers( + tarball <- renv_bootstrap_download(version), + error = function(err) { + catf("FAILED") + stop("failed to download:\n", conditionMessage(err)) + } + ) + catf("OK") + on.exit(unlink(tarball), add = TRUE) + + # now attempt to install + catf("- Installing renv ... ", appendLF = FALSE) + withCallingHandlers( + status <- renv_bootstrap_install(version, tarball, library), + error = function(err) { + catf("FAILED") + stop("failed to install:\n", conditionMessage(err)) + } + ) + catf("OK") + + # add empty line to break up bootstrapping from normal output + catf("") + return(invisible()) + } + + renv_bootstrap_tests_running <- function() { + getOption("renv.tests.running", default = FALSE) + } + + renv_bootstrap_repos <- function() { + + # get CRAN repository + cran <- getOption("renv.repos.cran", "https://cloud.r-project.org") + + # check for repos override + repos <- Sys.getenv("RENV_CONFIG_REPOS_OVERRIDE", unset = NA) + if (!is.na(repos)) { + + # split on ';' if present + parts <- strsplit(repos, ";", fixed = TRUE)[[1L]] + + # split into named repositories if present + idx <- regexpr("=", parts, fixed = TRUE) + keys <- substring(parts, 1L, idx - 1L) + vals <- substring(parts, idx + 1L) + names(vals) <- keys + + # if we have a single unnamed repository, call it CRAN + if (length(vals) == 1L && identical(keys, "")) + names(vals) <- "CRAN" + + return(vals) + + } + + # check for lockfile repositories + repos <- tryCatch(renv_bootstrap_repos_lockfile(), error = identity) + if (!inherits(repos, "error") && length(repos)) + return(repos) + + # retrieve current repos + repos <- getOption("repos") + + # ensure @CRAN@ entries are resolved + repos[repos == "@CRAN@"] <- cran + + # add in renv.bootstrap.repos if set + default <- c(FALLBACK = "https://cloud.r-project.org") + extra <- getOption("renv.bootstrap.repos", default = default) + repos <- c(repos, extra) + + # remove duplicates that might've snuck in + dupes <- duplicated(repos) | duplicated(names(repos)) + repos[!dupes] + + } + + renv_bootstrap_repos_lockfile <- function() { + + lockpath <- Sys.getenv("RENV_PATHS_LOCKFILE", unset = "renv.lock") + if (!file.exists(lockpath)) + return(NULL) + + lockfile <- tryCatch(renv_json_read(lockpath), error = identity) + if (inherits(lockfile, "error")) { + warning(lockfile) + return(NULL) + } + + repos <- lockfile$R$Repositories + if (length(repos) == 0) + return(NULL) + + keys <- vapply(repos, `[[`, "Name", FUN.VALUE = character(1)) + vals <- vapply(repos, `[[`, "URL", FUN.VALUE = character(1)) + names(vals) <- keys + + return(vals) + + } + + renv_bootstrap_download <- function(version) { + + sha <- attr(version, "sha", exact = TRUE) + + methods <- if (!is.null(sha)) { + + # attempting to bootstrap a development version of renv + c( + function() renv_bootstrap_download_tarball(sha), + function() renv_bootstrap_download_github(sha) + ) + + } else { + + # attempting to bootstrap a release version of renv + c( + function() renv_bootstrap_download_tarball(version), + function() renv_bootstrap_download_cran_latest(version), + function() renv_bootstrap_download_cran_archive(version) + ) + + } + + for (method in methods) { + path <- tryCatch(method(), error = identity) + if (is.character(path) && file.exists(path)) + return(path) + } + + stop("All download methods failed") + + } + + renv_bootstrap_download_impl <- function(url, destfile) { + + mode <- "wb" + + # https://bugs.r-project.org/bugzilla/show_bug.cgi?id=17715 + fixup <- + Sys.info()[["sysname"]] == "Windows" && + substring(url, 1L, 5L) == "file:" + + if (fixup) + mode <- "w+b" + + args <- list( + url = url, + destfile = destfile, + mode = mode, + quiet = TRUE + ) + + if ("headers" %in% names(formals(utils::download.file))) { + headers <- renv_bootstrap_download_custom_headers(url) + if (length(headers) && is.character(headers)) + args$headers <- headers + } + + do.call(utils::download.file, args) + + } + + renv_bootstrap_download_custom_headers <- function(url) { + + headers <- getOption("renv.download.headers") + if (is.null(headers)) + return(character()) + + if (!is.function(headers)) + stopf("'renv.download.headers' is not a function") + + headers <- headers(url) + if (length(headers) == 0L) + return(character()) + + if (is.list(headers)) + headers <- unlist(headers, recursive = FALSE, use.names = TRUE) + + ok <- + is.character(headers) && + is.character(names(headers)) && + all(nzchar(names(headers))) + + if (!ok) + stop("invocation of 'renv.download.headers' did not return a named character vector") + + headers + + } + + renv_bootstrap_download_cran_latest <- function(version) { + + spec <- renv_bootstrap_download_cran_latest_find(version) + type <- spec$type + repos <- spec$repos + + baseurl <- utils::contrib.url(repos = repos, type = type) + ext <- if (identical(type, "source")) + ".tar.gz" + else if (Sys.info()[["sysname"]] == "Windows") + ".zip" + else + ".tgz" + name <- sprintf("renv_%s%s", version, ext) + url <- paste(baseurl, name, sep = "/") + + destfile <- file.path(tempdir(), name) + status <- tryCatch( + renv_bootstrap_download_impl(url, destfile), + condition = identity + ) + + if (inherits(status, "condition")) + return(FALSE) + + # report success and return + destfile + + } + + renv_bootstrap_download_cran_latest_find <- function(version) { + + # check whether binaries are supported on this system + binary <- + getOption("renv.bootstrap.binary", default = TRUE) && + !identical(.Platform$pkgType, "source") && + !identical(getOption("pkgType"), "source") && + Sys.info()[["sysname"]] %in% c("Darwin", "Windows") + + types <- c(if (binary) "binary", "source") + + # iterate over types + repositories + for (type in types) { + for (repos in renv_bootstrap_repos()) { + + # build arguments for utils::available.packages() call + args <- list(type = type, repos = repos) + + # add custom headers if available -- note that + # utils::available.packages() will pass this to download.file() + if ("headers" %in% names(formals(utils::download.file))) { + headers <- renv_bootstrap_download_custom_headers(repos) + if (length(headers) && is.character(headers)) + args$headers <- headers + } + + # retrieve package database + db <- tryCatch( + as.data.frame( + do.call(utils::available.packages, args), + stringsAsFactors = FALSE + ), + error = identity + ) + + if (inherits(db, "error")) + next + + # check for compatible entry + entry <- db[db$Package %in% "renv" & db$Version %in% version, ] + if (nrow(entry) == 0) + next + + # found it; return spec to caller + spec <- list(entry = entry, type = type, repos = repos) + return(spec) + + } + } + + # if we got here, we failed to find renv + fmt <- "renv %s is not available from your declared package repositories" + stop(sprintf(fmt, version)) + + } + + renv_bootstrap_download_cran_archive <- function(version) { + + name <- sprintf("renv_%s.tar.gz", version) + repos <- renv_bootstrap_repos() + urls <- file.path(repos, "src/contrib/Archive/renv", name) + destfile <- file.path(tempdir(), name) + + for (url in urls) { + + status <- tryCatch( + renv_bootstrap_download_impl(url, destfile), + condition = identity + ) + + if (identical(status, 0L)) + return(destfile) + + } + + return(FALSE) + + } + + renv_bootstrap_find <- function(version) { + + path <- renv_bootstrap_find_cache(version) + if (length(path) && file.exists(path)) { + catf("- Using renv %s from global package cache", version) + return(path) + } + + } + + renv_bootstrap_find_cache <- function(version) { + + md5 <- attr(version, "md5", exact = TRUE) + if (is.null(md5)) + return() + + # infer path to renv cache + cache <- Sys.getenv("RENV_PATHS_CACHE", unset = "") + if (!nzchar(cache)) { + root <- Sys.getenv("RENV_PATHS_ROOT", unset = NA) + if (!is.na(root)) + cache <- file.path(root, "cache") + } + + if (!nzchar(cache)) { + tools <- asNamespace("tools") + if (is.function(tools$R_user_dir)) { + root <- tools$R_user_dir("renv", "cache") + cache <- file.path(root, "cache") + } + } + + # start completing path to cache + file.path( + cache, + renv_bootstrap_cache_version(), + renv_bootstrap_platform_prefix(), + "renv", + version, + md5, + "renv" + ) + + } + + renv_bootstrap_download_tarball <- function(version) { + + # if the user has provided the path to a tarball via + # an environment variable, then use it + tarball <- Sys.getenv("RENV_BOOTSTRAP_TARBALL", unset = NA) + if (is.na(tarball)) + return() + + # allow directories + if (dir.exists(tarball)) { + name <- sprintf("renv_%s.tar.gz", version) + tarball <- file.path(tarball, name) + } + + # bail if it doesn't exist + if (!file.exists(tarball)) { + + # let the user know we weren't able to honour their request + fmt <- "- RENV_BOOTSTRAP_TARBALL is set (%s) but does not exist." + msg <- sprintf(fmt, tarball) + warning(msg) + + # bail + return() + + } + + catf("- Using local tarball '%s'.", tarball) + tarball + + } + + renv_bootstrap_github_token <- function() { + for (envvar in c("GITHUB_TOKEN", "GITHUB_PAT", "GH_TOKEN")) { + envval <- Sys.getenv(envvar, unset = NA) + if (!is.na(envval)) + return(envval) + } + } + + renv_bootstrap_download_github <- function(version) { + + enabled <- Sys.getenv("RENV_BOOTSTRAP_FROM_GITHUB", unset = "TRUE") + if (!identical(enabled, "TRUE")) + return(FALSE) + + # prepare download options + token <- renv_bootstrap_github_token() + if (is.null(token)) + token <- "" + + if (nzchar(Sys.which("curl")) && nzchar(token)) { + fmt <- "--location --fail --header \"Authorization: token %s\"" + extra <- sprintf(fmt, token) + saved <- options("download.file.method", "download.file.extra") + options(download.file.method = "curl", download.file.extra = extra) + on.exit(do.call(base::options, saved), add = TRUE) + } else if (nzchar(Sys.which("wget")) && nzchar(token)) { + fmt <- "--header=\"Authorization: token %s\"" + extra <- sprintf(fmt, token) + saved <- options("download.file.method", "download.file.extra") + options(download.file.method = "wget", download.file.extra = extra) + on.exit(do.call(base::options, saved), add = TRUE) + } + + url <- file.path("https://api.github.com/repos/rstudio/renv/tarball", version) + name <- sprintf("renv_%s.tar.gz", version) + destfile <- file.path(tempdir(), name) + + status <- tryCatch( + renv_bootstrap_download_impl(url, destfile), + condition = identity + ) + + if (!identical(status, 0L)) + return(FALSE) + + renv_bootstrap_download_augment(destfile) + + return(destfile) + + } + + # Add Sha to DESCRIPTION. This is stop gap until #890, after which we + # can use renv::install() to fully capture metadata. + renv_bootstrap_download_augment <- function(destfile) { + sha <- renv_bootstrap_git_extract_sha1_tar(destfile) + if (is.null(sha)) { + return() + } + + # Untar + tempdir <- tempfile("renv-github-") + on.exit(unlink(tempdir, recursive = TRUE), add = TRUE) + untar(destfile, exdir = tempdir) + pkgdir <- dir(tempdir, full.names = TRUE)[[1]] + + # Modify description + desc_path <- file.path(pkgdir, "DESCRIPTION") + desc_lines <- readLines(desc_path) + remotes_fields <- c( + "RemoteType: github", + "RemoteHost: api.github.com", + "RemoteRepo: renv", + "RemoteUsername: rstudio", + "RemotePkgRef: rstudio/renv", + paste("RemoteRef: ", sha), + paste("RemoteSha: ", sha) + ) + writeLines(c(desc_lines[desc_lines != ""], remotes_fields), con = desc_path) + + # Re-tar + local({ + old <- setwd(tempdir) + on.exit(setwd(old), add = TRUE) + + tar(destfile, compression = "gzip") + }) + invisible() + } + + # Extract the commit hash from a git archive. Git archives include the SHA1 + # hash as the comment field of the tarball pax extended header + # (see https://www.kernel.org/pub/software/scm/git/docs/git-archive.html) + # For GitHub archives this should be the first header after the default one + # (512 byte) header. + renv_bootstrap_git_extract_sha1_tar <- function(bundle) { + + tryCatch( + renv_bootstrap_git_extract_sha1_tar_impl(bundle), + error = function(cnd) { + catf("- Failed to extract the Git SHA from '%s': %s", bundle, conditionMessage(cnd)) + NULL + } + ) + + } + + renv_bootstrap_git_extract_sha1_tar_impl <- function(bundle) { + + # open the bundle for reading + # We use gzcon for everything because (from ?gzcon) + # > Reading from a connection which does not supply a 'gzip' magic + # > header is equivalent to reading from the original connection + conn <- gzcon(file(bundle, open = "rb", raw = TRUE)) + on.exit(close(conn)) + + # The default pax header is 512 bytes long and the first pax extended header + # with the comment should be 51 bytes long + # `52 comment=` (11 chars) + 40 byte SHA1 hash + len <- 0x200 + 0x33 + res <- rawToChar(readBin(conn, "raw", n = len)[0x201:len]) + + if (grepl("^52 comment=", res)) { + sub("52 comment=", "", res) + } else { + NULL + } + + } + + renv_bootstrap_install <- function(version, tarball, library) { + + # attempt to install it into project library + dir.create(library, showWarnings = FALSE, recursive = TRUE) + output <- renv_bootstrap_install_impl(library, tarball) + + # check for successful install + status <- attr(output, "status") + if (is.null(status) || identical(status, 0L)) + return(status) + + # an error occurred; report it + header <- "installation of renv failed" + lines <- paste(rep.int("=", nchar(header)), collapse = "") + text <- paste(c(header, lines, output), collapse = "\n") + stop(text) + + } + + renv_bootstrap_install_impl <- function(library, tarball) { + + # invoke using system2 so we can capture and report output + bin <- R.home("bin") + exe <- if (Sys.info()[["sysname"]] == "Windows") "R.exe" else "R" + R <- file.path(bin, exe) + + args <- c( + "--vanilla", "CMD", "INSTALL", "--no-multiarch", + "-l", shQuote(path.expand(library)), + shQuote(path.expand(tarball)) + ) + + system2(R, args, stdout = TRUE, stderr = TRUE) + + } + + renv_bootstrap_platform_prefix_default <- function() { + + # read version component + version <- Sys.getenv("RENV_PATHS_VERSION", unset = "R-%v") + + # expand placeholders + placeholders <- list( + list("%v", format(getRversion()[1, 1:2])), + list("%V", format(getRversion()[1, 1:3])) + ) + + for (placeholder in placeholders) + version <- gsub(placeholder[[1L]], placeholder[[2L]], version, fixed = TRUE) + + # include SVN revision for development versions of R + # (to avoid sharing platform-specific artefacts with released versions of R) + devel <- + identical(R.version[["status"]], "Under development (unstable)") || + identical(R.version[["nickname"]], "Unsuffered Consequences") + + if (devel) + version <- paste(version, R.version[["svn rev"]], sep = "-r") + + version + + } + + renv_bootstrap_platform_prefix <- function() { + + # construct version prefix + version <- renv_bootstrap_platform_prefix_default() + + # build list of path components + components <- c(version, R.version$platform) + + # include prefix if provided by user + prefix <- renv_bootstrap_platform_prefix_impl() + if (!is.na(prefix) && nzchar(prefix)) + components <- c(prefix, components) + + # build prefix + paste(components, collapse = "/") + + } + + renv_bootstrap_platform_prefix_impl <- function() { + + # if an explicit prefix has been supplied, use it + prefix <- Sys.getenv("RENV_PATHS_PREFIX", unset = NA) + if (!is.na(prefix)) + return(prefix) + + # if the user has requested an automatic prefix, generate it + auto <- Sys.getenv("RENV_PATHS_PREFIX_AUTO", unset = NA) + if (is.na(auto) && getRversion() >= "4.4.0") + auto <- "TRUE" + + if (auto %in% c("TRUE", "True", "true", "1")) + return(renv_bootstrap_platform_prefix_auto()) + + # empty string on failure + "" + + } + + renv_bootstrap_platform_prefix_auto <- function() { + + prefix <- tryCatch(renv_bootstrap_platform_os(), error = identity) + if (inherits(prefix, "error") || prefix %in% "unknown") { + + msg <- paste( + "failed to infer current operating system", + "please file a bug report at https://github.com/rstudio/renv/issues", + sep = "; " + ) + + warning(msg) + + } + + prefix + + } + + renv_bootstrap_platform_os <- function() { + + sysinfo <- Sys.info() + sysname <- sysinfo[["sysname"]] + + # handle Windows + macOS up front + if (sysname == "Windows") + return("windows") + else if (sysname == "Darwin") + return("macos") + + # check for os-release files + for (file in c("/etc/os-release", "/usr/lib/os-release")) + if (file.exists(file)) + return(renv_bootstrap_platform_os_via_os_release(file, sysinfo)) + + # check for redhat-release files + if (file.exists("/etc/redhat-release")) + return(renv_bootstrap_platform_os_via_redhat_release()) + + "unknown" + + } + + renv_bootstrap_platform_os_via_os_release <- function(file, sysinfo) { + + # read /etc/os-release + release <- utils::read.table( + file = file, + sep = "=", + quote = c("\"", "'"), + col.names = c("Key", "Value"), + comment.char = "#", + stringsAsFactors = FALSE + ) + + vars <- as.list(release$Value) + names(vars) <- release$Key + + # get os name + os <- tolower(sysinfo[["sysname"]]) + + # read id + id <- "unknown" + for (field in c("ID", "ID_LIKE")) { + if (field %in% names(vars) && nzchar(vars[[field]])) { + id <- vars[[field]] + break + } + } + + # read version + version <- "unknown" + for (field in c("UBUNTU_CODENAME", "VERSION_CODENAME", "VERSION_ID", "BUILD_ID")) { + if (field %in% names(vars) && nzchar(vars[[field]])) { + version <- vars[[field]] + break + } + } + + # join together + paste(c(os, id, version), collapse = "-") + + } + + renv_bootstrap_platform_os_via_redhat_release <- function() { + + # read /etc/redhat-release + contents <- readLines("/etc/redhat-release", warn = FALSE) + + # infer id + id <- if (grepl("centos", contents, ignore.case = TRUE)) + "centos" + else if (grepl("redhat", contents, ignore.case = TRUE)) + "redhat" + else + "unknown" + + # try to find a version component (very hacky) + version <- "unknown" + + parts <- strsplit(contents, "[[:space:]]")[[1L]] + for (part in parts) { + + nv <- tryCatch(numeric_version(part), error = identity) + if (inherits(nv, "error")) + next + + version <- nv[1, 1] + break + + } + + paste(c("linux", id, version), collapse = "-") + + } + + renv_bootstrap_library_root_name <- function(project) { + + # use project name as-is if requested + asis <- Sys.getenv("RENV_PATHS_LIBRARY_ROOT_ASIS", unset = "FALSE") + if (asis) + return(basename(project)) + + # otherwise, disambiguate based on project's path + id <- substring(renv_bootstrap_hash_text(project), 1L, 8L) + paste(basename(project), id, sep = "-") + + } + + renv_bootstrap_library_root <- function(project) { + + prefix <- renv_bootstrap_profile_prefix() + + path <- Sys.getenv("RENV_PATHS_LIBRARY", unset = NA) + if (!is.na(path)) + return(paste(c(path, prefix), collapse = "/")) + + path <- renv_bootstrap_library_root_impl(project) + if (!is.null(path)) { + name <- renv_bootstrap_library_root_name(project) + return(paste(c(path, prefix, name), collapse = "/")) + } + + renv_bootstrap_paths_renv("library", project = project) + + } + + renv_bootstrap_library_root_impl <- function(project) { + + root <- Sys.getenv("RENV_PATHS_LIBRARY_ROOT", unset = NA) + if (!is.na(root)) + return(root) + + type <- renv_bootstrap_project_type(project) + if (identical(type, "package")) { + userdir <- renv_bootstrap_user_dir() + return(file.path(userdir, "library")) + } + + } + + renv_bootstrap_validate_version <- function(version, description = NULL) { + + # resolve description file + # + # avoid passing lib.loc to `packageDescription()` below, since R will + # use the loaded version of the package by default anyhow. note that + # this function should only be called after 'renv' is loaded + # https://github.com/rstudio/renv/issues/1625 + description <- description %||% packageDescription("renv") + + # check whether requested version 'version' matches loaded version of renv + sha <- attr(version, "sha", exact = TRUE) + valid <- if (!is.null(sha)) + renv_bootstrap_validate_version_dev(sha, description) + else + renv_bootstrap_validate_version_release(version, description) + + if (valid) + return(TRUE) + + # the loaded version of renv doesn't match the requested version; + # give the user instructions on how to proceed + dev <- identical(description[["RemoteType"]], "github") + remote <- if (dev) + paste("rstudio/renv", description[["RemoteSha"]], sep = "@") + else + paste("renv", description[["Version"]], sep = "@") + + # display both loaded version + sha if available + friendly <- renv_bootstrap_version_friendly( + version = description[["Version"]], + sha = if (dev) description[["RemoteSha"]] + ) + + fmt <- heredoc(" + renv %1$s was loaded from project library, but this project is configured to use renv %2$s. + - Use `renv::record(\"%3$s\")` to record renv %1$s in the lockfile. + - Use `renv::restore(packages = \"renv\")` to install renv %2$s into the project library. + ") + catf(fmt, friendly, renv_bootstrap_version_friendly(version), remote) + + FALSE + + } + + renv_bootstrap_validate_version_dev <- function(version, description) { + + expected <- description[["RemoteSha"]] + if (!is.character(expected)) + return(FALSE) + + pattern <- sprintf("^\\Q%s\\E", version) + grepl(pattern, expected, perl = TRUE) + + } + + renv_bootstrap_validate_version_release <- function(version, description) { + expected <- description[["Version"]] + is.character(expected) && identical(c(expected), c(version)) + } + + renv_bootstrap_hash_text <- function(text) { + + hashfile <- tempfile("renv-hash-") + on.exit(unlink(hashfile), add = TRUE) + + writeLines(text, con = hashfile) + tools::md5sum(hashfile) + + } + + renv_bootstrap_load <- function(project, libpath, version) { + + # try to load renv from the project library + if (!requireNamespace("renv", lib.loc = libpath, quietly = TRUE)) + return(FALSE) + + # warn if the version of renv loaded does not match + renv_bootstrap_validate_version(version) + + # execute renv load hooks, if any + hooks <- getHook("renv::autoload") + for (hook in hooks) + if (is.function(hook)) + tryCatch(hook(), error = warnify) + + # load the project + renv::load(project) + + TRUE + + } + + renv_bootstrap_profile_load <- function(project) { + + # if RENV_PROFILE is already set, just use that + profile <- Sys.getenv("RENV_PROFILE", unset = NA) + if (!is.na(profile) && nzchar(profile)) + return(profile) + + # check for a profile file (nothing to do if it doesn't exist) + path <- renv_bootstrap_paths_renv("profile", profile = FALSE, project = project) + if (!file.exists(path)) + return(NULL) + + # read the profile, and set it if it exists + contents <- readLines(path, warn = FALSE) + if (length(contents) == 0L) + return(NULL) + + # set RENV_PROFILE + profile <- contents[[1L]] + if (!profile %in% c("", "default")) + Sys.setenv(RENV_PROFILE = profile) + + profile + + } + + renv_bootstrap_profile_prefix <- function() { + profile <- renv_bootstrap_profile_get() + if (!is.null(profile)) + return(file.path("profiles", profile, "renv")) + } + + renv_bootstrap_profile_get <- function() { + profile <- Sys.getenv("RENV_PROFILE", unset = "") + renv_bootstrap_profile_normalize(profile) + } + + renv_bootstrap_profile_set <- function(profile) { + profile <- renv_bootstrap_profile_normalize(profile) + if (is.null(profile)) + Sys.unsetenv("RENV_PROFILE") + else + Sys.setenv(RENV_PROFILE = profile) + } + + renv_bootstrap_profile_normalize <- function(profile) { + + if (is.null(profile) || profile %in% c("", "default")) + return(NULL) + + profile + + } + + renv_bootstrap_path_absolute <- function(path) { + + substr(path, 1L, 1L) %in% c("~", "/", "\\") || ( + substr(path, 1L, 1L) %in% c(letters, LETTERS) && + substr(path, 2L, 3L) %in% c(":/", ":\\") + ) + + } + + renv_bootstrap_paths_renv <- function(..., profile = TRUE, project = NULL) { + renv <- Sys.getenv("RENV_PATHS_RENV", unset = "renv") + root <- if (renv_bootstrap_path_absolute(renv)) NULL else project + prefix <- if (profile) renv_bootstrap_profile_prefix() + components <- c(root, renv, prefix, ...) + paste(components, collapse = "/") + } + + renv_bootstrap_project_type <- function(path) { + + descpath <- file.path(path, "DESCRIPTION") + if (!file.exists(descpath)) + return("unknown") + + desc <- tryCatch( + read.dcf(descpath, all = TRUE), + error = identity + ) + + if (inherits(desc, "error")) + return("unknown") + + type <- desc$Type + if (!is.null(type)) + return(tolower(type)) + + package <- desc$Package + if (!is.null(package)) + return("package") + + "unknown" + + } + + renv_bootstrap_user_dir <- function() { + dir <- renv_bootstrap_user_dir_impl() + path.expand(chartr("\\", "/", dir)) + } + + renv_bootstrap_user_dir_impl <- function() { + + # use local override if set + override <- getOption("renv.userdir.override") + if (!is.null(override)) + return(override) + + # use R_user_dir if available + tools <- asNamespace("tools") + if (is.function(tools$R_user_dir)) + return(tools$R_user_dir("renv", "cache")) + + # try using our own backfill for older versions of R + envvars <- c("R_USER_CACHE_DIR", "XDG_CACHE_HOME") + for (envvar in envvars) { + root <- Sys.getenv(envvar, unset = NA) + if (!is.na(root)) + return(file.path(root, "R/renv")) + } + + # use platform-specific default fallbacks + if (Sys.info()[["sysname"]] == "Windows") + file.path(Sys.getenv("LOCALAPPDATA"), "R/cache/R/renv") + else if (Sys.info()[["sysname"]] == "Darwin") + "~/Library/Caches/org.R-project.R/R/renv" + else + "~/.cache/R/renv" + + } + + renv_bootstrap_version_friendly <- function(version, shafmt = NULL, sha = NULL) { + sha <- sha %||% attr(version, "sha", exact = TRUE) + parts <- c(version, sprintf(shafmt %||% " [sha: %s]", substring(sha, 1L, 7L))) + paste(parts, collapse = "") + } + + renv_bootstrap_exec <- function(project, libpath, version) { + if (!renv_bootstrap_load(project, libpath, version)) + renv_bootstrap_run(project, libpath, version) + } + + renv_bootstrap_run <- function(project, libpath, version) { + tryCatch( + renv_bootstrap_run_impl(project, libpath, version), + error = function(e) { + msg <- paste( + "failed to bootstrap renv: the project will not be loaded.", + paste("Reason:", conditionMessage(e)), + "Use `renv::activate()` to re-initialize the project.", + sep = "\n" + ) + warning(msg, call. = FALSE) + } + ) + } + + renv_bootstrap_run_impl <- function(project, libpath, version) { + + # perform bootstrap + bootstrap(version, libpath) + + # exit early if we're just testing bootstrap + if (!is.na(Sys.getenv("RENV_BOOTSTRAP_INSTALL_ONLY", unset = NA))) + return(TRUE) + + # try again to load + if (requireNamespace("renv", lib.loc = libpath, quietly = TRUE)) { + return(renv::load(project = project)) + } + + # failed to download or load renv; warn the user + msg <- c( + "Failed to find an renv installation: the project will not be loaded.", + "Use `renv::activate()` to re-initialize the project." + ) + + warning(paste(msg, collapse = "\n"), call. = FALSE) + + } + + renv_bootstrap_cache_version <- function() { + # NOTE: users should normally not override the cache version; + # this is provided just to make testing easier + Sys.getenv("RENV_CACHE_VERSION", unset = "v5") + } + + renv_bootstrap_cache_version_previous <- function() { + version <- renv_bootstrap_cache_version() + number <- as.integer(substring(version, 2L)) + paste("v", number - 1L, sep = "") + } + + renv_json_read <- function(file = NULL, text = NULL) { + + jlerr <- NULL + + # if jsonlite is loaded, use that instead + if ("jsonlite" %in% loadedNamespaces()) { + + json <- tryCatch(renv_json_read_jsonlite(file, text), error = identity) + if (!inherits(json, "error")) + return(json) + + jlerr <- json + + } + + # otherwise, fall back to the default JSON reader + json <- tryCatch(renv_json_read_default(file, text), error = identity) + if (!inherits(json, "error")) + return(json) + + # report an error + if (!is.null(jlerr)) + stop(jlerr) + else + stop(json) + + } + + renv_json_read_jsonlite <- function(file = NULL, text = NULL) { + text <- paste(text %||% readLines(file, warn = FALSE), collapse = "\n") + jsonlite::fromJSON(txt = text, simplifyVector = FALSE) + } + + renv_json_read_patterns <- function() { + + list( + + # objects + list("{", "\t\n\tobject(\t\n\t", TRUE), + list("}", "\t\n\t)\t\n\t", TRUE), + + # arrays + list("[", "\t\n\tarray(\t\n\t", TRUE), + list("]", "\n\t\n)\n\t\n", TRUE), + + # maps + list(":", "\t\n\t=\t\n\t", TRUE), + + # newlines + list("\\u000a", "\n", FALSE) + + ) + + } + + renv_json_read_envir <- function() { + + envir <- new.env(parent = emptyenv()) + + envir[["+"]] <- `+` + envir[["-"]] <- `-` + + envir[["object"]] <- function(...) { + result <- list(...) + names(result) <- as.character(names(result)) + result + } + + envir[["array"]] <- list + + envir[["true"]] <- TRUE + envir[["false"]] <- FALSE + envir[["null"]] <- NULL + + envir + + } + + renv_json_read_remap <- function(object, patterns) { + + # repair names if necessary + if (!is.null(names(object))) { + + nms <- names(object) + for (pattern in patterns) + nms <- gsub(pattern[[2L]], pattern[[1L]], nms, fixed = TRUE) + names(object) <- nms + + } + + # repair strings if necessary + if (is.character(object)) { + for (pattern in patterns) + object <- gsub(pattern[[2L]], pattern[[1L]], object, fixed = TRUE) + } + + # recurse for other objects + if (is.recursive(object)) + for (i in seq_along(object)) + object[i] <- list(renv_json_read_remap(object[[i]], patterns)) + + # return remapped object + object + + } + + renv_json_read_default <- function(file = NULL, text = NULL) { + + # read json text + text <- paste(text %||% readLines(file, warn = FALSE), collapse = "\n") + + # convert into something the R parser will understand + patterns <- renv_json_read_patterns() + transformed <- text + for (pattern in patterns) + transformed <- gsub(pattern[[1L]], pattern[[2L]], transformed, fixed = TRUE) + + # parse it + rfile <- tempfile("renv-json-", fileext = ".R") + on.exit(unlink(rfile), add = TRUE) + writeLines(transformed, con = rfile) + json <- parse(rfile, keep.source = FALSE, srcfile = NULL)[[1L]] + + # evaluate in safe environment + result <- eval(json, envir = renv_json_read_envir()) + + # fix up strings if necessary -- do so only with reversible patterns + patterns <- Filter(function(pattern) pattern[[3L]], patterns) + renv_json_read_remap(result, patterns) + + } + + + # load the renv profile, if any + renv_bootstrap_profile_load(project) + + # construct path to library root + root <- renv_bootstrap_library_root(project) + + # construct library prefix for platform + prefix <- renv_bootstrap_platform_prefix() + + # construct full libpath + libpath <- file.path(root, prefix) + + # run bootstrap code + renv_bootstrap_exec(project, libpath, version) + + invisible() + +}) diff --git a/applications/poll-studio/renv/settings.json b/applications/poll-studio/renv/settings.json new file mode 100644 index 00000000..6f21bb78 --- /dev/null +++ b/applications/poll-studio/renv/settings.json @@ -0,0 +1,22 @@ +{ + "bioconductor.enabled": true, + "bioconductor.version": null, + "external.libraries": [], + "ignored.packages": [], + "lockfile.sanitize": true, + "package.dependency.fields": [ + "Imports", + "Depends", + "LinkingTo" + ], + "ppm.enabled": null, + "ppm.ignored.urls": [], + "r.version": null, + "snapshot.dev": false, + "snapshot.type": "all", + "use.cache": true, + "vcs.ignore.cellar": true, + "vcs.ignore.library": true, + "vcs.ignore.local": true, + "vcs.manage.ignores": true +} diff --git a/applications/poll-studio/run.R b/applications/poll-studio/run.R new file mode 100644 index 00000000..22668212 --- /dev/null +++ b/applications/poll-studio/run.R @@ -0,0 +1,7 @@ +port <- suppressWarnings(as.integer(Sys.getenv("PORT", "4000"))) +if (is.na(port) || port < 1L || port > 65535L) stop("Invalid PORT") +options(shiny.sanitize.errors = TRUE, shiny.maxRequestSize = 65536) +tryCatch( + shiny::runApp(".", host = "127.0.0.1", port = port, launch.browser = FALSE), + interrupt = function(error) message("Poll Studio stopped after interrupt") +) diff --git a/applications/poll-studio/sql/bootstrap.sql b/applications/poll-studio/sql/bootstrap.sql new file mode 100644 index 00000000..7cb4a371 --- /dev/null +++ b/applications/poll-studio/sql/bootstrap.sql @@ -0,0 +1,22 @@ +\set ON_ERROR_STOP on +\getenv migration_password MIGRATION_PASSWORD +\getenv app_password APP_PASSWORD +SELECT length(:'migration_password') >= 24 AND length(:'app_password') >= 24 + AND :'migration_password' <> :'app_password' AS passwords_valid \gset +\if :passwords_valid +\else +\echo Use two distinct role passwords of at least 24 characters +\quit 1 +\endif +BEGIN; +CREATE ROLE polls_migration LOGIN PASSWORD :'migration_password'; +CREATE ROLE polls_app LOGIN PASSWORD :'app_password'; +REVOKE CREATE ON SCHEMA public FROM PUBLIC; +SELECT format('REVOKE CREATE, TEMPORARY ON DATABASE %I FROM PUBLIC', current_database()) \gexec +CREATE SCHEMA poll_studio AUTHORIZATION polls_migration; +REVOKE ALL ON SCHEMA poll_studio FROM PUBLIC; +GRANT USAGE ON SCHEMA poll_studio TO polls_app; +ALTER ROLE polls_app SET statement_timeout = '5s'; +ALTER ROLE polls_app SET lock_timeout = '3s'; +ALTER ROLE polls_app SET idle_in_transaction_session_timeout = '5s'; +COMMIT; diff --git a/applications/poll-studio/sql/cleanup.sql b/applications/poll-studio/sql/cleanup.sql new file mode 100644 index 00000000..350a3181 --- /dev/null +++ b/applications/poll-studio/sql/cleanup.sql @@ -0,0 +1,6 @@ +\set ON_ERROR_STOP on +BEGIN; +DROP SCHEMA IF EXISTS poll_studio CASCADE; +DROP ROLE IF EXISTS polls_app; +DROP ROLE IF EXISTS polls_migration; +COMMIT; diff --git a/applications/poll-studio/sql/migrate.sql b/applications/poll-studio/sql/migrate.sql new file mode 100644 index 00000000..f7ae0fb7 --- /dev/null +++ b/applications/poll-studio/sql/migrate.sql @@ -0,0 +1,47 @@ +\set ON_ERROR_STOP on +BEGIN; +CREATE TABLE IF NOT EXISTS poll_studio.schema_migrations ( + version integer PRIMARY KEY, + applied_at timestamptz NOT NULL DEFAULT clock_timestamp() +); +SELECT NOT EXISTS (SELECT 1 FROM poll_studio.schema_migrations WHERE version = 1) + AS apply_migration \gset +\if :apply_migration +CREATE TABLE poll_studio.polls ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + question varchar(200) NOT NULL CHECK ( + length(btrim(question)) BETWEEN 1 AND 200 AND question !~ '[[:cntrl:]]' + ), + created_at timestamptz NOT NULL DEFAULT clock_timestamp(), + closed_at timestamptz, + response_count integer NOT NULL DEFAULT 0 CHECK (response_count BETWEEN 0 AND 10000) +); +CREATE INDEX polls_recent ON poll_studio.polls (created_at DESC, id DESC); +CREATE TABLE poll_studio.choices ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + poll_id bigint NOT NULL REFERENCES poll_studio.polls (id), + position integer NOT NULL CHECK (position BETWEEN 1 AND 8), + label varchar(80) NOT NULL CHECK (length(btrim(label)) BETWEEN 1 AND 80 AND label !~ '[[:cntrl:]]'), + UNIQUE (poll_id, position), + UNIQUE (poll_id, label), + UNIQUE (poll_id, id) +); +CREATE TABLE poll_studio.responses ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + poll_id bigint NOT NULL REFERENCES poll_studio.polls (id), + choice_id bigint NOT NULL, + participant_code varchar(32) NOT NULL CHECK (participant_code ~ '^[A-Z0-9][A-Z0-9_-]{2,31}$'), + created_at timestamptz NOT NULL DEFAULT clock_timestamp(), + CONSTRAINT response_poll_choice FOREIGN KEY (poll_id, choice_id) + REFERENCES poll_studio.choices (poll_id, id), + CONSTRAINT response_poll_code UNIQUE (poll_id, participant_code) +); +CREATE INDEX responses_counts ON poll_studio.responses (poll_id, choice_id); +INSERT INTO poll_studio.schema_migrations (version) VALUES (1); +\else +\echo Migration already applied +\endif +GRANT SELECT, INSERT ON poll_studio.polls, poll_studio.choices, poll_studio.responses TO polls_app; +GRANT UPDATE (response_count, closed_at) ON poll_studio.polls TO polls_app; +GRANT USAGE ON ALL SEQUENCES IN SCHEMA poll_studio TO polls_app; +COMMIT; diff --git a/applications/poll-studio/sql/seed.sql b/applications/poll-studio/sql/seed.sql new file mode 100644 index 00000000..a2207afc --- /dev/null +++ b/applications/poll-studio/sql/seed.sql @@ -0,0 +1,13 @@ +\set ON_ERROR_STOP on +BEGIN; +SELECT NOT EXISTS (SELECT 1 FROM poll_studio.polls WHERE question = 'Which fictional workshop should we run?') + AS seed_needed \gset +\if :seed_needed +INSERT INTO poll_studio.polls (question) VALUES ('Which fictional workshop should we run?') + RETURNING id AS sample_poll_id \gset +INSERT INTO poll_studio.choices (poll_id, position, label) VALUES + (:sample_poll_id, 1, 'Paper planets'), + (:sample_poll_id, 2, 'Tiny gardens'), + (:sample_poll_id, 3, 'Quiet circuits'); +\endif +COMMIT; diff --git a/applications/poll-studio/test/browser.mjs b/applications/poll-studio/test/browser.mjs new file mode 100644 index 00000000..ec57da69 --- /dev/null +++ b/applications/poll-studio/test/browser.mjs @@ -0,0 +1,207 @@ +import assert from "node:assert/strict"; +import { spawn, execFile } from "node:child_process"; +import { once } from "node:events"; +import { mkdir } from "node:fs/promises"; +import { createServer } from "node:http"; +import { promisify } from "node:util"; +import { chromium } from "playwright"; +import WebSocket from "ws"; + +const execute = promisify(execFile); +const appDir = process.env.APP_DIR; +const evidence = process.env.EVIDENCE_DIR; +assert.ok(appDir && evidence); +await mkdir(evidence, { recursive: true }); +const port = 4083; +const origin = `http://127.0.0.1:${port}`; +const env = Object.fromEntries( + ["HOME", "PATH", "PGHOST", "PGPORT", "PGDATABASE", "PGUSER", "PGPASSWORD", "PGSSLROOTCERT"] + .map((key) => [key, process.env[key]]), +); +env.LANG = "C.UTF-8"; +env.PORT = String(port); +env.APP_ORIGIN = origin; +const rscript = process.env.RSCRIPT || "/home/al/R/bin/Rscript"; +let processHandle; +let browser; +let external; +let checks = 0; +const pass = (message) => console.log(`PASS ${++checks}: ${message}`); +const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); +async function start() { + const child = spawn(rscript, ["run.R"], { cwd: appDir, env, stdio: ["ignore", "pipe", "pipe"] }); + child.stdout.on("data", (data) => process.stdout.write(data)); + child.stderr.on("data", (data) => process.stderr.write(data)); + processHandle = child; + for (let attempt = 0; attempt < 150; attempt++) { + if (child.exitCode !== null) throw new Error("Shiny exited during startup"); + try { + const response = await fetch(origin); + if (response.ok) return child.pid; + } catch { /* Wait for the actual listener. */ } + await sleep(100); + } + throw new Error("Shiny startup timed out"); +} +async function stop() { + const child = processHandle; + if (!child || child.exitCode !== null) return; + const exited = once(child, "exit"); + child.kill("SIGINT"); + let forced = false; + const timeout = setTimeout(() => { forced = true; child.kill("SIGKILL"); }, 10000); + const [code, signal] = await exited; + clearTimeout(timeout); + assert.equal(forced, false, "Shiny must actually exit after interrupt"); + assert.equal(code, 0, `Unexpected shutdown ${signal}`); + processHandle = undefined; +} +async function connected(page) { + await page.goto(origin); + await page.waitForFunction(() => window.Shiny?.shinyapp?.isConnected()); + await page.locator("#poll_id option").first().waitFor({ state: "attached" }); +} +async function waitText(page, selector, text) { + await page.waitForFunction(({ selector, text }) => + document.querySelector(selector)?.textContent.includes(text), { selector, text }); +} +async function createPoll(page, question, choices) { + await page.locator("#question").fill(question); + await page.locator("#choice_lines").fill(choices.join("\n")); + await page.locator("#create").click(); + await waitText(page, "#summary_heading", question); + return page.locator("#poll_id").inputValue(); +} +async function storedQuestionCount(question) { + const { stdout } = await execute(rscript, ["test/observe.R", question], { cwd: appDir, env }); + return JSON.parse(stdout); +} +function missingOriginAttempt(question) { + return new Promise((resolve, reject) => { + const socket = new WebSocket(`ws://127.0.0.1:${port}/websocket`); + const timeout = setTimeout(() => { socket.terminate(); reject(new Error("Missing Origin did not close")); }, 5000); + socket.on("error", reject); + socket.on("open", () => socket.send(JSON.stringify({ method: "init", data: { + question, choice_lines: "One\nTwo", "create:shiny.action": 1, + } }))); + socket.on("close", () => { clearTimeout(timeout); resolve(); }); + }); +} + +try { + const firstPid = await start(); + browser = await chromium.launch({ headless: true }); + const contextA = await browser.newContext({ viewport: { width: 1440, height: 1100 } }); + const contextB = await browser.newContext({ viewport: { width: 1440, height: 1100 } }); + const pageA = await contextA.newPage(); + const pageB = await contextB.newPage(); + const errors = []; + pageA.on("pageerror", (error) => errors.push(error.message)); + pageB.on("pageerror", (error) => errors.push(error.message)); + await connected(pageA); + const questionA = `Browser workshop ${Date.now()}`; + const idA = await createPoll(pageA, questionA, ["Paper planets", "Tiny gardens", "Quiet circuits"]); + assert.match(idA, /^[1-9][0-9]*$/); + await pageA.locator("#code").fill("sample_b01"); + await pageA.locator("#respond").click(); + await waitText(pageA, "#summary_heading", "1 saved responses"); + await waitText(pageA, "#last_result", "Response saved once."); + await pageA.waitForFunction(() => document.querySelector("#counts img")?.naturalWidth > 0); + await pageA.screenshot({ path: `${evidence}/desktop.png`, fullPage: true }); + pass("real browser creates choices, saves one response and renders stored chart/counts"); + + await connected(pageB); + await pageB.locator("#poll_id").selectOption(idA); + await waitText(pageB, "#summary_heading", questionA); + await pageA.locator("#respond").click(); + await waitText(pageA, "#last_result", "Matching saved response replayed."); + await pageA.getByLabel("Tiny gardens", { exact: true }).check(); + await pageA.locator("#respond").click(); + await pageA.getByText("This code already selected a different choice.", { exact: true }).waitFor(); + await pageB.locator("#refresh").click(); + await waitText(pageB, "#summary_heading", "1 saved responses"); + pass("matching code replays, changed choice conflicts and another context refreshes committed counts"); + + await pageB.locator("#close").click(); + await waitText(pageB, "#poll_state", "Closed"); + await pageA.locator("#refresh").click(); + await waitText(pageA, "#poll_state", "Closed"); + await pageA.getByLabel("Paper planets", { exact: true }).check(); + await pageA.locator("#respond").click(); + await waitText(pageA, "#last_result", "Matching saved response replayed."); + await pageA.locator("#code").fill("new_after_close"); + await pageA.locator("#respond").click(); + await pageA.getByText("Poll is closed; matching saved responses can still replay.", { exact: true }).waitFor(); + await waitText(pageA, "#summary_heading", "1 saved responses"); + pass("close coordinates stored state and preserves matching replay while denying a new response"); + + const questionB = `Second browser poll ${Date.now()}`; + const idB = await createPoll(pageA, questionB, ["Garden ", "Paper"]); + assert.notEqual(idB, idA); + await waitText(pageA, "#poll_state", "Open"); + assert.equal(await pageA.locator("#choice_id img").count(), 0); + await pageA.locator("#code").fill("sample_second"); + await pageA.locator("#respond").click(); + await waitText(pageA, "#summary_heading", "1 saved responses"); + pass("second creation selects the new poll and labels remain escaped"); + + const questionC = `Eight choices ${Date.now()}`; + const longLabels = Array.from({ length: 8 }, (_, index) => + `Option ${index + 1} ${"abcdefghijkl".repeat(7)}`.slice(0, 80)); + await createPoll(pageA, questionC, longLabels); + await pageA.locator("#code").fill("sample_long"); + await pageA.locator("#respond").click(); + await waitText(pageA, "#summary_heading", "1 saved responses"); + await pageA.setViewportSize({ width: 390, height: 844 }); + await waitText(pageA, "#count_table", longLabels[7]); + await pageA.waitForFunction(() => { + const image = document.querySelector("#counts img"); + return image && Math.abs(image.naturalWidth - document.querySelector("#counts").clientWidth) < 5; + }); + await pageA.waitForFunction(() => document.querySelector("#counts img")?.naturalWidth > 0); + await pageA.screenshot({ path: `${evidence}/mobile.png`, fullPage: true }); + assert.ok(await pageA.evaluate(() => document.documentElement.scrollWidth <= innerWidth)); + pass("eight maximum-length labels retain full table text and a bounded mobile chart/layout"); + + external = createServer((_request, response) => { response.end("Origin fixture"); }); + await new Promise((resolve) => external.listen(4084, "127.0.0.1", resolve)); + const outsider = await contextB.newPage(); + await outsider.goto("http://127.0.0.1:4084"); + const foreignQuestion = `FOREIGN_ORIGIN_${Date.now()}`; + const closed = await outsider.evaluate(({ port, question }) => new Promise((resolve, reject) => { + const socket = new WebSocket(`ws://127.0.0.1:${port}/websocket`); + const timeout = setTimeout(() => { socket.close(); reject(new Error("Foreign Origin remained open")); }, 5000); + socket.onopen = () => socket.send(JSON.stringify({ method: "init", data: { + question, choice_lines: "One\nTwo", "create:shiny.action": 1, + } })); + socket.onclose = () => { clearTimeout(timeout); resolve(true); }; + socket.onerror = () => { clearTimeout(timeout); reject(new Error("WebSocket transport failed")); }; + }), { port, question: foreignQuestion }); + assert.equal(closed, true); + const missingQuestion = `MISSING_ORIGIN_${Date.now()}`; + await missingOriginAttempt(missingQuestion); + assert.equal(await storedQuestionCount(foreignQuestion), 0); + assert.equal(await storedQuestionCount(missingQuestion), 0); + assert.equal(await storedQuestionCount(questionA), 1); + pass("actual foreign-browser and missing-Origin WebSockets close without a write; real-browser control exists"); + + await stop(); + const secondPid = await start(); + assert.notEqual(secondPid, firstPid); + await connected(pageA); + await waitText(pageA, "#summary_heading", questionC); + await waitText(pageA, "#summary_heading", "1 saved responses"); + await pageA.locator("#code").fill("sample_long"); + await pageA.locator("#respond").click(); + await waitText(pageA, "#last_result", "Matching saved response replayed."); + assert.deepEqual(errors, []); + pass("original R process exits before replacement and stored poll/response replay survives restart"); + console.log(`Browser acceptance: ${checks} checks passed; no browser errors`); +} catch (error) { + console.error(error); + throw error; +} finally { + if (external) await new Promise((resolve) => external.close(resolve)); + if (browser) await browser.close(); + await stop(); +} diff --git a/applications/poll-studio/test/cloud.R b/applications/poll-studio/test/cloud.R new file mode 100644 index 00000000..910f195d --- /dev/null +++ b/applications/poll-studio/test/cloud.R @@ -0,0 +1,226 @@ +source("R/validation.R") +source("R/database.R") +source("R/polls.R") +source("test/domain.R") + +evidence <- required_env("EVIDENCE_DIR") +dir.create(evidence, recursive = TRUE, showWarnings = FALSE) +opts <- connection_options() +db <- new_pool() +owner_opts <- opts +owner_opts$user <- "polls_migration" +owner_opts$password <- required_env("MIGRATION_PASSWORD") +owner <- do.call(DBI::dbConnect, owner_opts) +admin_opts <- opts +admin_opts$user <- required_env("ADMIN_USER") +admin_opts$password <- required_env("ADMIN_PASSWORD") +admin_opts$options <- "-c statement_timeout=30000 -c lock_timeout=30000 -c idle_in_transaction_session_timeout=30000" +admin <- do.call(DBI::dbConnect, admin_opts) + +cloud_check <- function(name, action) { + action() + cat("CLOUD PASS:", name, "\n") +} +choices_for <- function(id) poll_summary(db, id)$choice_id +make_poll <- function(label) create_poll(db, paste(label, format(Sys.time(), "%Y%m%d%H%M%OS6")), c("Garden", "Paper")) +assert_db_error <- function(action, pattern) { + error <- tryCatch({ action(); NULL }, error = identity) + stopifnot(inherits(error, "error"), grepl(pattern, conditionMessage(error), ignore.case = TRUE)) + invisible(error) +} +spawn_worker <- function(task, poll_id, choice_id = "1", code = "PLACEHOLDER", suffix) { + output <- file.path(evidence, paste0(suffix, ".json")) + unlink(output) + system2(file.path(R.home("bin"), "Rscript"), + args = vapply(c("test/worker.R", task, poll_id, choice_id, code, output), shQuote, character(1)), + stdout = file.path(evidence, paste0(suffix, ".log")), + stderr = file.path(evidence, paste0(suffix, ".log")), wait = FALSE) + output +} +await_result <- function(path) { + for (attempt in seq_len(200L)) { + if (file.exists(path)) return(jsonlite::read_json(path, simplifyVector = TRUE)) + Sys.sleep(0.1) + } + stop("Independent R worker timed out") +} +wait_blocked <- function(number) { + for (attempt in seq_len(100L)) { + DBI::dbExecute(admin, "SELECT pg_stat_clear_snapshot()") + rows <- DBI::dbGetQuery(admin, + "SELECT count(*)::integer AS n FROM pg_stat_activity + WHERE application_name = 'poll-studio' AND cardinality(pg_blocking_pids(pid)) > 0") + if (rows$n[[1]] >= number) return(invisible(TRUE)) + Sys.sleep(0.1) + } + stop("Did not observe expected independent blocked database sessions") +} +lock_poll <- function(id) { + DBI::dbBegin(admin) + DBI::dbGetQuery(admin, "SELECT id FROM poll_studio.polls WHERE id = $1::bigint FOR UPDATE", + params = list(id)) +} + +tryCatch({ + cloud_check("positive verified connection, atomic creation and stored chart counts", function() { + stopifnot(DBI::dbGetQuery(db, "SELECT current_user AS role")$role == "polls_app") + id <- make_poll("Creation") + choices <- choices_for(id) + stopifnot(length(choices) == 2L) + first <- record_response(db, id, choices[[1]], "sample_001") + repeat_result <- record_response(db, id, choices[[1]], "SAMPLE_001") + stopifnot(!first$replayed, repeat_result$replayed, identical(first$id, repeat_result$id)) + expect_error(function() record_response(db, id, choices[[2]], "SAMPLE_001"), "poll_conflict") + close_poll(db, id) + retained <- record_response(db, id, choices[[1]], "SAMPLE_001") + stopifnot(retained$replayed, identical(first$id, retained$id)) + expect_error(function() record_response(db, id, choices[[1]], "NEW_CODE"), "poll_conflict") + summary <- poll_summary(db, id) + stopifnot(sum(summary$responses) == 1L, all(summary$response_count == 1L), all(summary$closed)) + }) + + cloud_check("second-write creation and response failures roll back earlier changes", function() { + DBI::dbExecute(owner, + "CREATE FUNCTION poll_studio.reject_test_choice() RETURNS trigger LANGUAGE plpgsql AS $$ + BEGIN IF NEW.label = 'FORCE_REJECT_CHOICE' THEN RAISE EXCEPTION 'test choice rejection'; END IF; + RETURN NEW; END $$") + DBI::dbExecute(owner, "CREATE TRIGGER reject_test_choice BEFORE INSERT ON poll_studio.choices + FOR EACH ROW EXECUTE FUNCTION poll_studio.reject_test_choice()") + question <- "Atomic rollback test fixture" + assert_db_error(function() create_poll(db, question, c("Allowed first", "FORCE_REJECT_CHOICE")), "test choice rejection") + stopifnot(DBI::dbGetQuery(db, "SELECT count(*)::integer AS n FROM poll_studio.polls WHERE question = $1", + params = list(question))$n == 0L) + stopifnot(DBI::dbGetQuery(db, "SELECT count(*)::integer AS n FROM poll_studio.choices WHERE label = 'Allowed first'")$n == 0L) + DBI::dbExecute(owner, "DROP TRIGGER reject_test_choice ON poll_studio.choices") + DBI::dbExecute(owner, "DROP FUNCTION poll_studio.reject_test_choice()") + id <- make_poll("Response rollback") + choice <- choices_for(id)[[1]] + DBI::dbExecute(owner, + "CREATE FUNCTION poll_studio.reject_test_response() RETURNS trigger LANGUAGE plpgsql AS $$ + BEGIN IF NEW.participant_code = 'FAIL_RESPONSE' THEN RAISE EXCEPTION 'test response rejection'; END IF; + RETURN NEW; END $$") + DBI::dbExecute(owner, "CREATE TRIGGER reject_test_response BEFORE INSERT ON poll_studio.responses + FOR EACH ROW EXECUTE FUNCTION poll_studio.reject_test_response()") + assert_db_error(function() record_response(db, id, choice, "FAIL_RESPONSE"), "test response rejection") + stopifnot(all(poll_summary(db, id)$response_count == 0L), sum(poll_summary(db, id)$responses) == 0L) + DBI::dbExecute(owner, "DROP TRIGGER reject_test_response ON poll_studio.responses") + DBI::dbExecute(owner, "DROP FUNCTION poll_studio.reject_test_response()") + record_response(db, id, choice, "FAIL_RESPONSE") + stopifnot(sum(poll_summary(db, id)$responses) == 1L) + }) + + cloud_check("composite foreign key, natural-key uniqueness and actual role grants", function() { + first <- make_poll("Foreign first") + second <- make_poll("Foreign second") + choice <- choices_for(second)[[1]] + assert_db_error(function() DBI::dbExecute(db, + "INSERT INTO poll_studio.responses (poll_id, choice_id, participant_code) VALUES ($1::bigint, $2::bigint, 'FOREIGN_TEST')", + params = list(first, choice)), "response_poll_choice") + choice <- choices_for(first)[[1]] + record_response(db, first, choice, "UNIQUE_TEST") + assert_db_error(function() DBI::dbExecute(db, + "INSERT INTO poll_studio.responses (poll_id, choice_id, participant_code) VALUES ($1::bigint, $2::bigint, 'UNIQUE_TEST')", + params = list(first, choice)), "response_poll_code") + for (query in c("UPDATE poll_studio.responses SET participant_code = 'CHANGED'", + "DELETE FROM poll_studio.responses", "UPDATE poll_studio.choices SET label = 'CHANGED'", + "DELETE FROM poll_studio.choices", "UPDATE poll_studio.polls SET question = 'CHANGED'", + "DELETE FROM poll_studio.polls", "CREATE TABLE poll_studio.forbidden (id integer)", + "CREATE SCHEMA forbidden", "CREATE TEMP TABLE forbidden (id integer)", + "SELECT * FROM poll_studio.schema_migrations")) { + assert_db_error(function() DBI::dbExecute(db, query), "permission denied") + } + }) + + cloud_check("independent same-code workers serialize and retain one response", function() { + id <- make_poll("Race repeat") + choice <- choices_for(id)[[1]] + lock_poll(id) + a <- spawn_worker("respond", id, choice, "RACE_CODE", "same-a") + b <- spawn_worker("respond", id, choice, "RACE_CODE", "same-b") + wait_blocked(2L) + DBI::dbCommit(admin) + one <- await_result(a) + two <- await_result(b) + stopifnot(is.null(one$error), is.null(two$error), identical(one$id, two$id), + xor(one$replayed, two$replayed), sum(poll_summary(db, id)$responses) == 1L, + all(poll_summary(db, id)$response_count == 1L)) + }) + + cloud_check("held parent lock proves close-first and response-first ordering", function() { + id <- make_poll("Close first") + choice <- choices_for(id)[[1]] + lock_poll(id) + waiting <- spawn_worker("respond", id, choice, "LATE_CODE", "close-first") + wait_blocked(1L) + DBI::dbExecute(admin, "UPDATE poll_studio.polls SET closed_at = clock_timestamp() WHERE id = $1::bigint", + params = list(id)) + DBI::dbCommit(admin) + result <- await_result(waiting) + stopifnot(identical(result$kind, "poll_conflict"), sum(poll_summary(db, id)$responses) == 0L) + id <- make_poll("Response first") + choice <- choices_for(id)[[1]] + lock_poll(id) + waiting <- spawn_worker("close", id, suffix = "response-first") + wait_blocked(1L) + DBI::dbExecute(admin, "UPDATE poll_studio.polls SET response_count = response_count + 1 WHERE id = $1::bigint", + params = list(id)) + DBI::dbExecute(admin, + "INSERT INTO poll_studio.responses (poll_id, choice_id, participant_code) VALUES ($1::bigint, $2::bigint, 'BEFORE_CLOSE')", + params = list(id, choice)) + DBI::dbCommit(admin) + stopifnot(isTRUE(await_result(waiting)$closed), sum(poll_summary(db, id)$responses) == 1L, + all(poll_summary(db, id)$closed)) + stopifnot(record_response(db, id, choice, "BEFORE_CLOSE")$replayed) + }) + + cloud_check("response cap, Unicode bounds and bounded newest poll list", function() { + id <- make_poll("Capacity") + choice <- choices_for(id)[[1]] + record_response(db, id, choice, "RETAINED_CAP") + DBI::dbExecute(owner, "UPDATE poll_studio.polls SET response_count = 10000 WHERE id = $1::bigint", params = list(id)) + expect_error(function() record_response(db, id, choice, "OVER_CAP"), "poll_conflict") + stopifnot(record_response(db, id, choice, "RETAINED_CAP")$replayed) + # Owner-only cap acceleration; restore the truthful stored counter afterward. + DBI::dbExecute(owner, "UPDATE poll_studio.polls SET response_count = 1 WHERE id = $1::bigint", params = list(id)) + id <- create_poll(db, paste(rep("é", 200), collapse = ""), c("🌱", "Paper")) + stopifnot(nchar(poll_summary(db, id)$question[[1]]) == 200L) + for (i in seq_len(102L)) create_poll(db, paste("Bound fixture", i), c("One", "Two")) + rows <- list_polls(db) + stopifnot(nrow(rows) == 100L, !anyDuplicated(rows$id)) + }) + + cloud_check("certificate-specific CA and hostname failures with positive control", function() { + ca <- file.path(evidence, "wrong-ca.pem") + key <- file.path(evidence, "wrong-ca.key") + status <- system2("openssl", c("req", "-x509", "-newkey", "rsa:2048", "-nodes", "-days", "1", + "-subj", shQuote("/CN=Wrong Test CA"), "-keyout", shQuote(key), "-out", shQuote(ca)), + stdout = FALSE, stderr = FALSE) + stopifnot(status == 0L) + wrong <- opts + wrong$sslrootcert <- ca + error <- assert_db_error(function() do.call(DBI::dbConnect, wrong), "certificate verify failed") + cat("Wrong CA: certificate verify failed\n") + wrong <- opts + # Use the verified endpoint's DNS resolution, not the database's internal address. + address <- system2("getent", c("ahostsv4", shQuote(opts$host)), stdout = TRUE) + wrong$hostaddr <- strsplit(address[[1]], "[[:space:]]+")[[1]][[1]] + wrong$host <- "wrong-hostname.example.test" + assert_db_error(function() do.call(DBI::dbConnect, wrong), "does not match host name") + cat("Wrong hostname: real endpoint certificate does not match configured host\n") + positive <- do.call(DBI::dbConnect, opts) + stopifnot(DBI::dbGetQuery(positive, "SELECT 1 AS n")$n == 1L) + DBI::dbDisconnect(positive) + unlink(c(ca, key)) + }) + cat("Cloud acceptance: 7 checks passed\n") +}, finally = { + try(DBI::dbRollback(admin), silent = TRUE) + for (table in c("choices", "responses")) { + name <- if (table == "choices") "reject_test_choice" else "reject_test_response" + try(DBI::dbExecute(owner, paste0("DROP TRIGGER IF EXISTS ", name, " ON poll_studio.", table)), silent = TRUE) + try(DBI::dbExecute(owner, paste0("DROP FUNCTION IF EXISTS poll_studio.", name, "()")), silent = TRUE) + } + DBI::dbDisconnect(admin) + DBI::dbDisconnect(owner) + pool::poolClose(db) +}) diff --git a/applications/poll-studio/test/domain.R b/applications/poll-studio/test/domain.R new file mode 100644 index 00000000..135ffaa7 --- /dev/null +++ b/applications/poll-studio/test/domain.R @@ -0,0 +1,42 @@ +source("R/validation.R") +expect_error <- function(action, class = "poll_error") { + result <- tryCatch({ action(); NULL }, error = identity) + stopifnot(inherits(result, class)) +} +check <- function(name, action) { + action() + cat("PASS:", name, "\n") +} +check("bounded trimmed Unicode question and distinct choices", function() { + x <- validate_poll(" A small question ", c(" Garden ", "Paper")) + stopifnot(identical(x$question, "A small question"), identical(x$choices, c("Garden", "Paper"))) + stopifnot(nchar(validate_poll(paste(rep("é", 200), collapse = ""), c("A", "B"))$question) == 200) + expect_error(function() validate_poll(paste(rep("é", 201), collapse = ""), c("A", "B"))) +}) +check("malformed polls and control characters reject", function() { + for (choices in list("One", rep("x", 9), c(" A ", "A"), c("A", "\nB"), c("", "B"))) { + expect_error(function() validate_poll("Question", choices)) + } + expect_error(function() validate_poll(list("not text"), c("A", "B"))) +}) +check("synthetic codes normalize and exclude unsupported characters", function() { + stopifnot(identical(participant_code(" sample_001 "), "SAMPLE_001")) + for (value in c("AB", "A B", "a@b", "\tSAMPLE", paste(rep("A", 33), collapse = ""))) { + expect_error(function() participant_code(value)) + } +}) +check("identifiers preserve bigint precision and signed range", function() { + stopifnot(identical(identifier("9223372036854775807"), "9223372036854775807")) + for (value in list("9223372036854775808", "0", "1e3", 1, "1; DROP TABLE", NA_character_)) { + expect_error(function() identifier(value)) + } +}) +check("actual request Origin requires exact configured loopback value", function() { + expected <- "http://127.0.0.1:4000" + stopifnot(trusted_origin(list(HTTP_ORIGIN = expected), expected)) + for (value in list(NULL, "null", "http://localhost:4000", "http://127.0.0.1:4001", + "https://example.test", c(expected, "https://example.test"))) { + stopifnot(!trusted_origin(list(HTTP_ORIGIN = value), expected)) + } +}) +cat("Domain acceptance: 5 checks passed\n") diff --git a/applications/poll-studio/test/observe.R b/applications/poll-studio/test/observe.R new file mode 100644 index 00000000..225c5b06 --- /dev/null +++ b/applications/poll-studio/test/observe.R @@ -0,0 +1,8 @@ +source("R/database.R") +args <- commandArgs(trailingOnly = TRUE) +stopifnot(length(args) == 1L) +conn <- do.call(DBI::dbConnect, connection_options()) +result <- DBI::dbGetQuery(conn, + "SELECT count(*)::integer AS n FROM poll_studio.polls WHERE question = $1", params = list(args[[1]])) +DBI::dbDisconnect(conn) +cat(jsonlite::toJSON(result$n[[1]], auto_unbox = TRUE)) diff --git a/applications/poll-studio/test/worker.R b/applications/poll-studio/test/worker.R new file mode 100644 index 00000000..47a6983e --- /dev/null +++ b/applications/poll-studio/test/worker.R @@ -0,0 +1,22 @@ +source("R/validation.R") +source("R/database.R") +source("R/polls.R") +args <- commandArgs(trailingOnly = TRUE) +stopifnot(length(args) == 5L) +options <- connection_options() +# Independent contention fixtures may hold a lock longer than the interactive timeout. +options$options <- "-c statement_timeout=30000 -c lock_timeout=30000 -c idle_in_transaction_session_timeout=30000" +db <- do.call(pool::dbPool, c(options, list(minSize = 1L, maxSize = 1L))) +result <- tryCatch({ + if (args[[1]] == "respond") { + record_response(db, args[[2]], args[[3]], args[[4]]) + } else if (args[[1]] == "close") { + close_poll(db, args[[2]]) + list(closed = TRUE) + } else stop("Unknown test task") +}, poll_error = function(error) list(error = conditionMessage(error), kind = class(error)[[1]]), + error = function(error) list(error = "unexpected database error", kind = "unexpected")) +pool::poolClose(db) +temporary <- paste0(args[[5]], ".tmp") +jsonlite::write_json(result, temporary, auto_unbox = TRUE) +stopifnot(file.rename(temporary, args[[5]])) diff --git a/applications/poll-studio/www/style.css b/applications/poll-studio/www/style.css new file mode 100644 index 00000000..38271a80 --- /dev/null +++ b/applications/poll-studio/www/style.css @@ -0,0 +1,36 @@ +:root { color-scheme: light; font-family: system-ui, sans-serif; color: #243b32; background: #f3f5f0; } +body { background: #f3f5f0; color: #243b32; font-size: 15px; } +.page-shell { max-width: 1320px; margin: 0 auto; padding: 30px 22px; } +.masthead { display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid #d4dfd2; padding-bottom: 24px; } +.brand { font-size: 19px; font-weight: 600; } +.badge { border: 1px solid #ccd8c7; border-radius: 24px; padding: 8px 14px; font-size: 12px; } +.intro { padding: 50px 0 35px; } +.eyebrow { text-transform: uppercase; font-size: 11px; letter-spacing: 2px; color: #678568; } +h1 { font-family: Georgia, serif; font-size: clamp(36px, 5vw, 52px); margin: 12px 0 18px; } +.intro p { color: #64766b; } +.workspace { display: grid; grid-template-columns: 1fr 1fr 1.35fr; gap: 22px; align-items: start; } +.panel { background: white; padding: 25px; border: 1px solid #d7e0d3; border-radius: 14px; min-width: 0; } +h2 { font-size: 20px; margin: 0 0 24px; font-weight: 500; } +.form-control { border-color: #ccd8c7; border-radius: 6px; box-shadow: none; } +.shiny-input-container { width: 100%; } +.btn { background: #f4f6ef; border: 1px solid #ccd8c7; color: #344f41; border-radius: 6px; padding: 10px 13px; white-space: normal; } +.btn.primary { background: #2f5845; color: white; width: 100%; } +.hint { font-size: 12px; color: #748274; line-height: 1.6; margin-top: 18px; } +.state { font-size: 12px; background: #edf3e8; border-radius: 6px; padding: 10px; margin: 8px 0 20px; } +.state.closed { background: #f3ece0; } +.toolbar { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 24px; } +.footnote { border-top: 1px solid #d4dfd2; margin-top: 38px; padding-top: 20px; font-size: 12px; color: #748274; } +pre { white-space: pre-wrap; word-break: normal; background: #edf3e8; border: none; font-size: 12px; } +@media (max-width: 1050px) { .workspace { grid-template-columns: 1fr 1fr; } .results { grid-column: 1 / -1; } } +@media (max-width: 650px) { .workspace { display: block; } .panel { margin-bottom: 18px; } .page-shell { padding: 20px 14px; } .intro { padding: 30px 0; } } + +table { width: 100%; table-layout: fixed; } +td, th { overflow-wrap: anywhere; } + +#summary_heading { overflow-wrap: anywhere; } + +.radio label { overflow-wrap: anywhere; } + +#counts img { max-width: 100%; height: auto !important; } + +#count_table th:first-child, #count_table td:first-child { width: 70%; } From 6380abfb2bfe22eef691e02240287addc8516f4d Mon Sep 17 00:00:00 2001 From: sdairs Date: Fri, 2 Oct 2026 17:55:45 +0100 Subject: [PATCH 2/4] Resolve browser harness Rscript through PATH --- applications/poll-studio/README.md | 2 +- applications/poll-studio/test/browser.mjs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/applications/poll-studio/README.md b/applications/poll-studio/README.md index ad33866e..1815b468 100644 --- a/applications/poll-studio/README.md +++ b/applications/poll-studio/README.md @@ -145,7 +145,7 @@ Rscript test/cloud.R The suite uses separate R worker processes for same-code contention and held-lock response/close ordering, observing actual blocked database sessions. Workers use longer timeouts only for controlled test barriers. Owner-only temporary rejecting triggers force failures after earlier writes, then are removed; no runtime privilege changes are needed. The cap test accelerates the stored counter with the owner and restores it afterward. Foreign-choice and duplicate-key checks exercise actual database constraints. TLS tests use a valid unrelated CA and resolve the real endpoint address with a deliberately mismatched libpq hostname, then prove the ordinary connection succeeds. -Optional browser acceptance uses isolated Node24.21.0, Playwright1.63.0 and ws8.22.0 dependencies: +Optional browser acceptance uses isolated Node24.21.0, Playwright1.63.0 and ws8.22.0 dependencies. The harness resolves Rscript through PATH; set RSCRIPT to an explicit executable if needed: ```bash export BROWSER_DIR=/private/path/browser-dependencies diff --git a/applications/poll-studio/test/browser.mjs b/applications/poll-studio/test/browser.mjs index ec57da69..ba625634 100644 --- a/applications/poll-studio/test/browser.mjs +++ b/applications/poll-studio/test/browser.mjs @@ -21,7 +21,7 @@ const env = Object.fromEntries( env.LANG = "C.UTF-8"; env.PORT = String(port); env.APP_ORIGIN = origin; -const rscript = process.env.RSCRIPT || "/home/al/R/bin/Rscript"; +const rscript = process.env.RSCRIPT || "Rscript"; let processHandle; let browser; let external; From b6ca27224669a26cc17c8ff97be38b4df93b7316 Mon Sep 17 00:00:00 2001 From: sdairs Date: Fri, 2 Oct 2026 19:11:23 +0100 Subject: [PATCH 3/4] Keep tied poll IDs in numeric order --- applications/poll-studio/R/polls.R | 2 +- applications/poll-studio/README.md | 2 ++ applications/poll-studio/test/ordering.R | 33 ++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 applications/poll-studio/test/ordering.R diff --git a/applications/poll-studio/R/polls.R b/applications/poll-studio/R/polls.R index 29864daf..6b8c5270 100644 --- a/applications/poll-studio/R/polls.R +++ b/applications/poll-studio/R/polls.R @@ -16,7 +16,7 @@ create_poll <- function(db, question, choices) { list_polls <- function(db) { DBI::dbGetQuery(db, "SELECT id::text AS id, question, closed_at IS NOT NULL AS closed, response_count, - created_at FROM poll_studio.polls ORDER BY created_at DESC, id DESC LIMIT 100") + created_at FROM poll_studio.polls ORDER BY created_at DESC, poll_studio.polls.id DESC LIMIT 100") } poll_summary <- function(db, poll_id) { diff --git a/applications/poll-studio/README.md b/applications/poll-studio/README.md index 1815b468..83144a51 100644 --- a/applications/poll-studio/README.md +++ b/applications/poll-studio/README.md @@ -187,3 +187,5 @@ Deletion destroys that service's data; preserve needed evidence first. - [Shiny1.14.0 session WebSocket request source](https://github.com/rstudio/shiny/blob/v1.14.0/R/shiny.R) - [Standalone Shiny runApp](https://shiny.posit.co/r/reference/shiny/latest/runapp.html) - [ClickHouse Managed Postgres setup](https://clickhouse.com/docs/products/managed-postgres/quickstart) + +A targeted tie-order regression is available as `Rscript test/ordering.R` after fresh bootstrap/migration without seed. It creates 12 owner fixture polls with the same timestamp and complete choices, then calls the actual runtime pool and verifies numeric descending IDs. The list qualifies `poll_studio.polls.id` so the `id::text AS id` output alias cannot change the tie-breaker to lexical order. Use only a dedicated fixture; clean up with administrator credentials as above. diff --git a/applications/poll-studio/test/ordering.R b/applications/poll-studio/test/ordering.R new file mode 100644 index 00000000..c2b712f3 --- /dev/null +++ b/applications/poll-studio/test/ordering.R @@ -0,0 +1,33 @@ +# Targeted Cloud tie-order regression. Use a freshly migrated dedicated fixture, no seed. +source("R/validation.R") +source("R/database.R") +source("R/polls.R") + +opts <- connection_options() +owner_opts <- opts +owner_opts$user <- "polls_migration" +owner_opts$password <- required_env("MIGRATION_PASSWORD") +owner <- do.call(DBI::dbConnect, owner_opts) +db <- new_pool() +tryCatch({ + stopifnot(DBI::dbGetQuery(owner, "SELECT count(*)::integer AS n FROM poll_studio.polls")$n == 0L) + DBI::dbExecute(owner, + "INSERT INTO poll_studio.polls(question, created_at) + SELECT 'Tied fixture ' || n, '2026-10-02 00:00:00+00'::timestamptz + FROM generate_series(1, 12) AS g(n)") + DBI::dbExecute(owner, + "INSERT INTO poll_studio.choices(poll_id, position, label) + SELECT p.id, c.position, c.label FROM poll_studio.polls p + CROSS JOIN (VALUES(1, 'Blue'), (2, 'Orange')) AS c(position, label)") + old <- DBI::dbGetQuery(owner, + "SELECT id::text AS id FROM poll_studio.polls ORDER BY created_at DESC, id DESC")$id + expected <- as.character(12:1) + stopifnot(!identical(old, expected)) + rows <- list_polls(db) + stopifnot(identical(rows$id, expected), nrow(rows) == 12L, + length(unique(rows$id)) == 12L, length(unique(rows$created_at)) == 1L) + cat("CLOUD ORDERING PASS: actual runtime pool returned", paste(rows$id, collapse = ","), "\n") +}, finally = { + pool::poolClose(db) + DBI::dbDisconnect(owner) +}) From e3396bbfe4b7746d3f1fabbe196ab1e32e546a65 Mon Sep 17 00:00:00 2001 From: sdairs Date: Fri, 2 Oct 2026 21:31:28 +0100 Subject: [PATCH 4/4] docs: remove public beta qualifier --- applications/poll-studio/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/poll-studio/README.md b/applications/poll-studio/README.md index 83144a51..c9b0c216 100644 --- a/applications/poll-studio/README.md +++ b/applications/poll-studio/README.md @@ -1,6 +1,6 @@ # Poll studio -A trusted local R/Shiny workbench creates a fictional poll, records synthetic responses, draws stored counts, refreshes another browser's changes and closes the poll. ClickHouse Managed Postgres (public beta) stores the durable poll, choices and responses. Participant codes deduplicate submissions; they are not authenticated identities. All operators share authority. Use made-up labels/codes and no personal information. +A trusted local R/Shiny workbench creates a fictional poll, records synthetic responses, draws stored counts, refreshes another browser's changes and closes the poll. ClickHouse Managed Postgres stores the durable poll, choices and responses. Participant codes deduplicate submissions; they are not authenticated identities. All operators share authority. Use made-up labels/codes and no personal information. ## Boundaries and behavior