From b569697d6c343785cd248f6b4a4d4e0f6d56079a Mon Sep 17 00:00:00 2001 From: DanilSilin Date: Mon, 17 Aug 2026 18:43:31 +0300 Subject: [PATCH] fix(release): validate inputs before qualification Recognize legacy and elapsed-time completion markers during technical validation. Validate all seven scientific inputs before unit tests or production cases and preserve the input-contract log as release evidence. --- R/utils.R | 22 ++++++ .../reference/contracts/release-validation.md | 9 ++- scripts/run_release_qualification.R | 67 +++++++++++++++++++ .../validate_multicase_technical_exports.R | 8 +-- tests/testthat/test-release-edge-cases.R | 41 ++++++++++++ tests/testthat/test-utils.R | 18 +++++ 6 files changed, 158 insertions(+), 7 deletions(-) diff --git a/R/utils.R b/R/utils.R index 8028991..5fc7405 100644 --- a/R/utils.R +++ b/R/utils.R @@ -94,6 +94,28 @@ cancerppir_elapsed_label <- function( ) } +############################################################################## +cancerppir_log_has_completion_marker <- function(log_lines) { + log_lines <- as.character(log_lines) + log_lines <- log_lines[!is.na(log_lines)] + + if (!length(log_lines)) { + return(FALSE) + } + + any( + grepl( + paste0( + "^\\[CancerPPIr\\]", + "(?: \\[\\+[0-9]+:[0-9]{2}:[0-9]{2}\\])?", + " Done\\.$" + ), + log_lines, + perl = TRUE + ) + ) +} + ############################################################################## msg <- function(...) { message( diff --git a/docs/reference/contracts/release-validation.md b/docs/reference/contracts/release-validation.md index 48642ef..4ef8348 100644 --- a/docs/reference/contracts/release-validation.md +++ b/docs/reference/contracts/release-validation.md @@ -4,7 +4,9 @@ The release qualification is the single production qualification gate for a CancerPPIr release candidate. It runs repository preflight checks, one complete -unit-test suite, and one seven-case production regression. +unit-test suite, and one seven-case production regression. Before any tests or +production cases start, all seven inputs must pass the strict scientific input +contract. ## Command @@ -46,6 +48,10 @@ The qualification requires all static release, documentation, publication readiness, reproducible-environment, repository-quality, and CLI checks to pass before the seven-case run begins. +The input-contract preflight validates all seven files together and fails before +unit tests or case execution if any file is incomplete, malformed, ambiguous, or +contains duplicate gene symbols. + ## Release evidence The output root contains: @@ -54,6 +60,7 @@ The output root contains: - `release_case_summary.csv` - `release_validation.csv` - `release_preflight_validation.csv` +- `release_input_contract.log` - `release_unit_tests.log` - `release_multicase.log` diff --git a/scripts/run_release_qualification.R b/scripts/run_release_qualification.R index 1b5312c..150166a 100644 --- a/scripts/run_release_qualification.R +++ b/scripts/run_release_qualification.R @@ -256,6 +256,7 @@ required_project_files <- file.path( c( "R/load_all.R", "scripts/run_unit_tests.R", + "scripts/validate_input_contract.R", "scripts/validate_multicase_outputs.R", "scripts/validate_release_contract.R", "scripts/validate_documentation_contract.R", @@ -401,6 +402,11 @@ unit_test_log_temporary <- tempfile( fileext = ".log" ) +input_contract_log_temporary <- tempfile( + pattern = "release_input_contract_", + fileext = ".log" +) + multicase_log_temporary <- tempfile( pattern = "release_multicase_", fileext = ".log" @@ -409,6 +415,7 @@ multicase_log_temporary <- tempfile( on.exit( unlink( c( + input_contract_log_temporary, unit_test_log_temporary, multicase_log_temporary ) @@ -416,6 +423,53 @@ on.exit( add = TRUE ) +message( + "[CancerPPIr release] Validating the strict input contract for all seven cases." +) + +input_contract_arguments <- c( + shQuote( + file.path( + project_root, + "scripts", + "validate_input_contract.R" + ) + ), + vapply( + file.path(input_root, case_map$input_file), + shQuote, + FUN.VALUE = character(1) + ) +) + +input_contract_status <- system2( + command = rscript_command, + args = input_contract_arguments, + stdout = input_contract_log_temporary, + stderr = input_contract_log_temporary, + wait = TRUE +) + +if ( + is.null(input_contract_status) || + is.na(input_contract_status) || + input_contract_status != 0L +) { + stop( + paste0( + "Seven-case input-contract preflight failed with exit status ", + input_contract_status, + ".\n\nLog tail:\n", + tail_log(input_contract_log_temporary) + ), + call. = FALSE + ) +} + +message( + "[CancerPPIr release] Seven-case input-contract preflight: PASS." +) + if (run_tests) { message( "[CancerPPIr release] Running the complete unit-test suite once." @@ -695,6 +749,17 @@ output_root <- normalizePath( mustWork = TRUE ) +invisible( + file.copy( + input_contract_log_temporary, + file.path( + output_root, + "release_input_contract.log" + ), + overwrite = TRUE + ) +) + invisible( file.copy( unit_test_log_temporary, @@ -1381,6 +1446,7 @@ rownames(case_summary) <- NULL summary_table <- data.frame( metric = c( + "input_contract", "unit_tests", "static_release_checks", "documentation_checks", @@ -1390,6 +1456,7 @@ summary_table <- data.frame( "execution_mode" ), value = c( + "PASS", if (run_tests) "PASS" else "SKIPPED", as.character( nrow(static_validation) diff --git a/scripts/validate_multicase_technical_exports.R b/scripts/validate_multicase_technical_exports.R index 9cb5f22..656b5f4 100644 --- a/scripts/validate_multicase_technical_exports.R +++ b/scripts/validate_multicase_technical_exports.R @@ -1127,12 +1127,8 @@ for (case_index in seq_len( encoding = "UTF-8" ) - pipeline_done <- any( - grepl( - "[CancerPPIr] Done.", - log_lines, - fixed = TRUE - ) + pipeline_done <- cancerppir_log_has_completion_marker( + log_lines ) internal_validation <- expected_evidence$validation diff --git a/tests/testthat/test-release-edge-cases.R b/tests/testthat/test-release-edge-cases.R index e151cdc..8fb33f2 100644 --- a/tests/testthat/test-release-edge-cases.R +++ b/tests/testthat/test-release-edge-cases.R @@ -195,6 +195,47 @@ testthat::test_that( } ) +testthat::test_that( + "release qualification validates inputs before tests or cases", + { + project_root <- Sys.getenv("CANCERPPIR_PROJECT_ROOT") + testthat::expect_true(nzchar(project_root)) + + release_text <- paste( + readLines( + file.path( + project_root, + "scripts", + "run_release_qualification.R" + ), + warn = FALSE, + encoding = "UTF-8" + ), + collapse = "\n" + ) + + input_position <- regexpr( + "Validating the strict input contract for all seven cases.", + release_text, + fixed = TRUE + )[[1L]] + + unit_position <- regexpr( + "Running the complete unit-test suite once.", + release_text, + fixed = TRUE + )[[1L]] + + testthat::expect_gt(input_position, 0L) + testthat::expect_gt(unit_position, input_position) + testthat::expect_match( + release_text, + "validate_input_contract.R", + fixed = TRUE + ) + } +) + testthat::test_that( "release edge case: zero p-values remain finite and parser-safe in GraphML", { diff --git a/tests/testthat/test-utils.R b/tests/testthat/test-utils.R index 1517efa..8e6f046 100644 --- a/tests/testthat/test-utils.R +++ b/tests/testthat/test-utils.R @@ -162,6 +162,24 @@ testthat::test_that("ranking and text helpers retain qualified behavior", { testthat::expect_identical("value" %||% "fallback", "value") }) +testthat::test_that("pipeline completion markers support legacy and timed logs", { + testthat::expect_true( + cancerppir_log_has_completion_marker("[CancerPPIr] Done.") + ) + + testthat::expect_true( + cancerppir_log_has_completion_marker( + "[CancerPPIr] [+00:03:15] Done." + ) + ) + + testthat::expect_false( + cancerppir_log_has_completion_marker( + "[CancerPPIr] [+00:03:15] Not done." + ) + ) +}) + testthat::test_that("candidate score requires five complete finite components", { degree <- c(1, 2, 4) betweenness <- c(0.1, 0.2, 0.5)