From 3e438e5f25705554efd401315b62ef9015025fe1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 17 Aug 2026 16:32:53 +0000 Subject: [PATCH 1/5] test: add true-parameter FIPC linking RMSE recovery gate Pin buyer-visible recovery error for copied anchors versus old-form estimates and for unique new-form items versus generating 2PL parameters. Does not change autoFIPC() arithmetic. Co-authored-by: Seongho Bae --- .../test-true-parameter-linking-rmse.R | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 tests/testthat/test-true-parameter-linking-rmse.R diff --git a/tests/testthat/test-true-parameter-linking-rmse.R b/tests/testthat/test-true-parameter-linking-rmse.R new file mode 100644 index 00000000..8dcfdbcd --- /dev/null +++ b/tests/testthat/test-true-parameter-linking-rmse.R @@ -0,0 +1,140 @@ +# True-parameter linking RMSE / recovery (buyer-visible quality gate). +# +# Existing fixtures pin the Kim (2006) FIPC *contract* (anchors copied and +# held fixed). They do not report recovery error of free new-form items +# against the generating parameters. This file adds that metric. +# +# Scale: simdata() defaults to N(0, 1) theta. autoFIPC() is called with +# forceNormalZeroOne = TRUE and freeMEAN = FALSE so the linked unique items +# stay on that same metric. Anchors are fixed to *estimated* old-form +# values, so unique-item RMSE versus truth includes ordinary calibration +# error plus the small scale discrepancy of those estimated anchors. +# +# This package still orchestrates FIPC in R and delegates estimation to +# mirt (Chalmers, 2012; Bock & Aitkin, 1981). There is no Rust or GPU +# numeric core; this test is the recovery gate, not a new estimator. +# +# References (APA 7th): +# Bock, R. D., & Aitkin, M. (1981). Marginal maximum likelihood +# estimation of item parameters: Application of an EM algorithm. +# Psychometrika, 46(4), 443-459. https://doi.org/10.1007/BF02293801 +# Chalmers, R. P. (2012). mirt: A multidimensional item response theory +# package for the R environment. Journal of Statistical Software, +# 48(6), 1-29. https://doi.org/10.18637/jss.v048.i06 +# Kim, S. (2006). A comparative study of IRT fixed parameter calibration +# methods. Journal of Educational Measurement, 43(4), 355-381. +# https://doi.org/10.1111/j.1745-3984.2006.00021.x + +rmse <- function(est, tru) { + sqrt(mean((as.numeric(est) - as.numeric(tru))^2)) +} + +item_ad_values <- function(vals, items) { + out <- numeric(0) + for (it in items) { + row <- vals[vals$item == it & vals$name %in% c("a1", "d"), c("name", "value")] + row <- row[match(c("a1", "d"), row$name), ] + out <- c(out, row$value) + } + out +} + +item_ad_truth <- function(a, d, idx) { + as.numeric(rbind(a[idx, 1], d[idx])) +} + +test_that("FIPC recovers generating parameters at bounded RMSE", { + skip_on_cran() + skip_if_not_installed("mirt") + + set.seed(20260817) + old_item_names <- paste0("old_", 1:8) + new_item_names <- paste0("new_", 1:8) + old_common_items <- old_item_names[1:5] + new_common_items <- new_item_names[1:5] + unique_idx <- 6:8 + + common_a <- c(0.90, 1.15, 1.30, 0.80, 1.05) + common_d <- c(-1.00, -0.35, 0.20, 0.80, -0.50) + old_a <- matrix(c(common_a, 0.85, 1.20, 0.70), ncol = 1) + old_d <- c(common_d, 0.40, -0.65, 0.25) + new_a <- matrix(c(common_a, 1.10, 0.85, 1.25), ncol = 1) + new_d <- c(common_d, -0.15, 0.55, -0.40) + + old_data <- as.data.frame(mirt::simdata( + a = old_a, + d = old_d, + itemtype = rep("2PL", length(old_item_names)), + N = 2000 + )) + new_data <- as.data.frame(mirt::simdata( + a = new_a, + d = new_d, + itemtype = rep("2PL", length(new_item_names)), + N = 2000 + )) + names(old_data) <- old_item_names + names(new_data) <- new_item_names + + old_model <- mirt::mirt( + old_data, + 1, + itemtype = "2PL", + method = "EM", + verbose = FALSE, + technical = list(NCYCLES = 500) + ) + new_model <- mirt::mirt( + new_data, + 1, + itemtype = "2PL", + method = "EM", + verbose = FALSE, + technical = list(NCYCLES = 500) + ) + + linked <- aFIPC::autoFIPC( + newformXData = new_model, + oldformYData = old_model, + newformCommonItemNames = new_common_items, + oldformCommonItemNames = old_common_items, + itemtype = "2PL", + checkIPD = FALSE, + tryEM = TRUE, + freeMEAN = FALSE, + forceNormalZeroOne = TRUE, + confirmCommonItems = TRUE + ) + + old_values <- mirt::mod2values(old_model) + linked_values <- mirt::mod2values(linked$LinkedModel) + + old_anchor_est <- item_ad_values(old_values, old_common_items) + linked_anchor_est <- item_ad_values(linked_values, new_common_items) + anchor_copy_rmse <- rmse(linked_anchor_est, old_anchor_est) + expect_lt(anchor_copy_rmse, 1e-6) + + old_recovery_rmse <- rmse( + old_anchor_est, + item_ad_truth(old_a, old_d, seq_along(old_common_items)) + ) + expect_lt(old_recovery_rmse, 0.40) + + unique_linked_est <- item_ad_values(linked_values, new_item_names[unique_idx]) + unique_linked_rmse <- rmse( + unique_linked_est, + item_ad_truth(new_a, new_d, unique_idx) + ) + expect_lt(unique_linked_rmse, 0.50) + + # Keep the numbers in the failure message so CI logs are buyer-readable. + expect_true( + is.finite(old_recovery_rmse) && is.finite(unique_linked_rmse), + info = sprintf( + "anchor_copy_rmse=%.6f old_recovery_rmse=%.4f unique_linked_rmse=%.4f", + anchor_copy_rmse, + old_recovery_rmse, + unique_linked_rmse + ) + ) +}) From f443a3e9e699fc00cbfb4208692e9353cac0a5c6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 17 Aug 2026 17:23:51 +0000 Subject: [PATCH 2/5] ci: retrigger Strix after GitHub API rate-limit flake The org Strix required workflow failed at visibility lookup: installation 141441800 exceeded the GitHub API rate limit (HTTP 403), so is_private was not true/false. No test change. Co-authored-by: Seongho Bae From f10f4d76a215ac5838e52ce4f54bb433cec7d894 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 17 Aug 2026 17:42:53 +0000 Subject: [PATCH 3/5] test: print RMSE metrics on every recovery assertion testthat 3 expect_lt() has no info= argument. Move the three RMSE gates onto expect_true(..., info = metrics) and include anchor_copy_rmse in the finite check so a CI failure always shows the buyer-visible numbers. Also retriggers Noema after a GitHub HTTP 503 flake on the previous head. Co-authored-by: Seongho Bae --- .../test-true-parameter-linking-rmse.R | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/tests/testthat/test-true-parameter-linking-rmse.R b/tests/testthat/test-true-parameter-linking-rmse.R index 8dcfdbcd..20c052dc 100644 --- a/tests/testthat/test-true-parameter-linking-rmse.R +++ b/tests/testthat/test-true-parameter-linking-rmse.R @@ -112,29 +112,33 @@ test_that("FIPC recovers generating parameters at bounded RMSE", { old_anchor_est <- item_ad_values(old_values, old_common_items) linked_anchor_est <- item_ad_values(linked_values, new_common_items) anchor_copy_rmse <- rmse(linked_anchor_est, old_anchor_est) - expect_lt(anchor_copy_rmse, 1e-6) old_recovery_rmse <- rmse( old_anchor_est, item_ad_truth(old_a, old_d, seq_along(old_common_items)) ) - expect_lt(old_recovery_rmse, 0.40) unique_linked_est <- item_ad_values(linked_values, new_item_names[unique_idx]) unique_linked_rmse <- rmse( unique_linked_est, item_ad_truth(new_a, new_d, unique_idx) ) - expect_lt(unique_linked_rmse, 0.50) - # Keep the numbers in the failure message so CI logs are buyer-readable. + # testthat 3 expect_lt() has no info=; keep every gate on expect_true() + # so CI logs always print the three RMSE numbers. + metrics <- sprintf( + "anchor_copy_rmse=%.6f old_recovery_rmse=%.4f unique_linked_rmse=%.4f", + anchor_copy_rmse, + old_recovery_rmse, + unique_linked_rmse + ) expect_true( - is.finite(old_recovery_rmse) && is.finite(unique_linked_rmse), - info = sprintf( - "anchor_copy_rmse=%.6f old_recovery_rmse=%.4f unique_linked_rmse=%.4f", - anchor_copy_rmse, - old_recovery_rmse, - unique_linked_rmse - ) + is.finite(anchor_copy_rmse) && + is.finite(old_recovery_rmse) && + is.finite(unique_linked_rmse), + info = metrics ) + expect_true(anchor_copy_rmse < 1e-6, info = metrics) + expect_true(old_recovery_rmse < 0.40, info = metrics) + expect_true(unique_linked_rmse < 0.50, info = metrics) }) From 6f15584271313abf536322688375bb20601ffc0f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 17 Aug 2026 18:22:26 +0000 Subject: [PATCH 4/5] ci: retrigger Noema after GitHub.com GraphQL 503 outage Required noema-review failed twice on gh api graphql HTTP 503 during the GitHub.com partial outage. Package checks (R CMD check, quality, strix, security) already passed. Empty commit retriggers the org required review now that GraphQL is answering again. Co-authored-by: Seongho Bae From af61177e0039a689c64d09c7f63275641ce7d537 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 17 Aug 2026 23:09:59 +0000 Subject: [PATCH 5/5] ci: retrigger Noema now that GitHub.com is operational Required noema-review still shows the HTTP 503 from the earlier GitHub.com outage. All package checks already passed. Empty commit retriggers the org required review after status returned to All Systems Operational. Co-authored-by: Seongho Bae