From c350ad3918eacc626acd266975b8b0364ab2130d Mon Sep 17 00:00:00 2001 From: spomlol <77454330+CedricHermansBIT@users.noreply.github.com> Date: Fri, 9 Oct 2026 18:37:04 +0200 Subject: [PATCH 1/5] Represent empty exact SIFTS maps safely and report unavailable experimental entries --- shinyRam/R/atlas-sifts.R | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/shinyRam/R/atlas-sifts.R b/shinyRam/R/atlas-sifts.R index 65ff549a..fcc9807f 100644 --- a/shinyRam/R/atlas-sifts.R +++ b/shinyRam/R/atlas-sifts.R @@ -20,7 +20,16 @@ ram_atlas_exact_sifts_map <- function(payload, pdb_id, entity_id, accession) { if(is.null(raw)) raw <- list() if(!is.list(raw) || length(raw)>50000L) stop("Invalid SIFTS residue mapping records.",call.=FALSE) - if(!length(raw)) return(ram_canonical_empty_map()) + if(!length(raw)) { + # The generic empty canonical map lacks the Atlas-specific observed + # column. An empty exact SIFTS response must still have its full schema + # so it can be summarized safely and reported as unavailable. + empty <- ram_canonical_empty_map() + empty$label_seq_id <- integer() + empty$mon_id <- character() + empty$observed <- logical() + return(empty) + } clean <- function(value) { if(is.null(value) || length(value)!=1L || is.na(value)) @@ -85,3 +94,19 @@ ram_atlas_sifts_summary <- function(mapping) { conflicting_residues=sum(conflicts), chains=unique(as.character(mapping$chain))) } + +# One validated entry point for browser-retrieved verification. An empty +# mapping is a legitimate archive result but cannot verify a structure. +# Callers catch the error per entity and can continue a queued batch. +ram_atlas_verified_sifts <- function(payload,pdb_id,entity_id,accession) { + mapping <- ram_atlas_exact_sifts_map(payload,pdb_id,entity_id,accession) + summary <- ram_atlas_sifts_summary(mapping) + if(!nrow(mapping)) + stop(sprintf(paste0( + "No exact SIFTS residue mapping was found for PDB %s, entity %s, ", + "and UniProt %s. This entry cannot be used for Atlas clustering; ", + "try another experimental entry."), + toupper(as.character(pdb_id)),as.character(entity_id), + ram_uniprot_accession(accession)),call.=FALSE) + list(mapping=mapping,summary=summary) +} From 39ebcbab6ef5821084ba9fa3f2dd82f14d8ecf54 Mon Sep 17 00:00:00 2001 From: spomlol <77454330+CedricHermansBIT@users.noreply.github.com> Date: Fri, 9 Oct 2026 18:37:20 +0200 Subject: [PATCH 2/5] Catch all Atlas exact mapping and summary failures per selected structure --- shinyRam/app.R | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/shinyRam/app.R b/shinyRam/app.R index 8db919f1..41c2a2ed 100644 --- a/shinyRam/app.R +++ b/shinyRam/app.R @@ -2170,16 +2170,20 @@ server <- function(input, output, session) { "Exact SIFTS mapping could not be retrieved." else as.character(value$message)) } else { - mapped <- tryCatch( - ram_atlas_exact_sifts_map(value, value$pdb_id, + # Map validation AND summary creation belong to one per-entity + # error boundary. PDBe can legitimately return zero exact matches. + # That must not throw out of this observer or stall a verification batch. + checked <- tryCatch( + ram_atlas_verified_sifts(value,value$pdb_id, value$entity_id,chosen$accession), error=function(e) { outcomes[[chosen$key]] <<- list(state="error", message=conditionMessage(e)) NULL }) - if(!is.null(mapped)) { - summary <- ram_atlas_sifts_summary(mapped) + if(!is.null(checked)) { + mapped <- checked$mapping + summary <- checked$summary outcomes[[chosen$key]] <- c(list(state="mapped", source=as.character(value$source),endpoint=as.character(value$endpoint), matched_sifts_rows=as.integer(value$matched_sifts_rows), From d2f0c7c6e74fa74ea4089905f01b25e1b2622099 Mon Sep 17 00:00:00 2001 From: spomlol <77454330+CedricHermansBIT@users.noreply.github.com> Date: Fri, 9 Oct 2026 18:37:53 +0200 Subject: [PATCH 3/5] Reproduce zero-row SIFTS crash and verify safe subsequent structures --- tests/atlas-sifts.R | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/atlas-sifts.R b/tests/atlas-sifts.R index 805e4bc5..04bee932 100644 --- a/tests/atlas-sifts.R +++ b/tests/atlas-sifts.R @@ -39,4 +39,35 @@ assert(inherits(try(ram_atlas_exact_sifts_map(wrong,"1ABC",1L,"P12345"), wrong <- payload;wrong$rows[[1L]]$uniprot_accession <- "Q99999" assert(inherits(try(ram_atlas_exact_sifts_map(wrong,"1ABC",1L,"P12345"), silent=TRUE),"try-error"),"Wrong accession accepted.") +# Real PDBe results can have an entity matching RCSB's UniProt reference +# search but no exact _pdbx_sifts_xref_db rows for that accession. +empty_payload <- payload +empty_payload$rows <- list() +empty_mapping <- ram_atlas_exact_sifts_map(empty_payload,"1ABC",1L,"P12345") +assert(is.data.frame(empty_mapping) && nrow(empty_mapping)==0L && + all(c("observed","label_seq_id","mon_id") %in% names(empty_mapping)), + "An empty exact mapping must retain its full typed Atlas schema.") +empty_summary <- ram_atlas_sifts_summary(empty_mapping) +assert(empty_summary$rows==0L && empty_summary$observed_residues==0L && + empty_summary$conflicting_residues==0L, + "Empty exact SIFTS results should be safe to summarize.") +unavailable <- tryCatch(ram_atlas_verified_sifts( + empty_payload,"1ABC",1L,"P12345"),error=function(e)e) +assert(inherits(unavailable,"error") && + grepl("No exact SIFTS residue mapping",conditionMessage(unavailable), + fixed=TRUE), + "An entity with no exact rows must be rejected with a useful message.") +checked <- ram_atlas_verified_sifts(payload,"1ABC",1L,"P12345") +assert(identical(checked$mapping,mapping) && + identical(checked$summary,info), + "Successful exact mapping and summary must remain unchanged.") +# Invalid records must stay rejected per entity without affecting a +# subsequent valid verification in the same batch. +bad <- payload; bad$rows <- list(list(uniprot_accession="P12345")) +assert(inherits(try(ram_atlas_verified_sifts( + bad,"1ABC",1L,"P12345"),silent=TRUE),"try-error"), + "Malformed exact mapping must fail in the guarded verifier.") +again <- ram_atlas_verified_sifts(payload,"1ABC",1L,"P12345") +assert(again$summary$observed_residues==2L, + "A rejected PDB entity must not affect the next valid entry.") message("Exact PDBe SIFTS residue mapping tests passed.") From b85fa2a0dbd074b6e2b6ec02a3ebf8af252cd41f Mon Sep 17 00:00:00 2001 From: spomlol <77454330+CedricHermansBIT@users.noreply.github.com> Date: Fri, 9 Oct 2026 18:38:19 +0200 Subject: [PATCH 4/5] Browser-test zero-row PDBe SIFTS as recoverable card error, then successful verification --- tests/atlas-browser.cjs | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/tests/atlas-browser.cjs b/tests/atlas-browser.cjs index a7e75e7c..8bba4bac 100644 --- a/tests/atlas-browser.cjs +++ b/tests/atlas-browser.cjs @@ -118,7 +118,7 @@ ${atoms}${component} } else if (url === "https://www.ebi.ac.uk/pdbe/static/entry/4ake_updated.cif") { request.respond({status:200, headers:{...cors,"content-type":"text/plain"}, - body:syntheticUpdatedCif(101,false).replace(/P00533/g,"P69441")}); + body:syntheticUpdatedCif(101,false).replace(/P00533/g,"P00000")}); } else if (url === "https://www.ebi.ac.uk/pdbe/static/entry/1crn_updated.cif") { const mockCif = syntheticUpdatedCif(101,false); request.respond({status:200, @@ -441,6 +441,44 @@ ${atoms}${component} await page.setViewport({width:390,height:844}); await page.screenshot({path:path.join(output,"atlas-inventory-mobile.png"), fullPage:true}); + // An RCSB-linked polymer entity can have NO matching exact PDBe + // SIFTS rows. Its card must explain the limitation without crashing + // the Shiny observer, and a subsequent valid verification must work. + await page.setViewport({width:1366,height:900}); + await page.$eval("#atlasAccession",element=>{ + element.value="P69441"; + element.dispatchEvent(new Event("input",{bubbles:true})); + element.dispatchEvent(new Event("change",{bubbles:true})); + }); + await page.click("#atlasDiscover"); + await page.waitForFunction(() => + document.querySelector("#atlasResults")?.textContent.includes("4AKE"), + {timeout:25000}); + await page.click(".ram-atlas-verify"); + await page.waitForFunction(() => + document.querySelector("#atlasResults")?.textContent.includes( + "SIFTS unavailable: No exact SIFTS residue mapping was found"), + {timeout:25000}); + const failedText=await page.$eval("#atlasResults",element=>element.textContent); + assert.match(failedText,/4AKE/); + assert.match(failedText,/cannot be used for Atlas clustering/); + assert.equal(await page.evaluate(()=> + document.querySelectorAll(".shiny-output-error").length),0, + "Zero exact SIFTS matches must not throw into an output or observer."); + await page.$eval("#atlasAccession",element=>{ + element.value="P00533"; + element.dispatchEvent(new Event("input",{bubbles:true})); + element.dispatchEvent(new Event("change",{bubbles:true})); + }); + await page.click("#atlasDiscover"); + await page.waitForFunction(() => + document.querySelector("#atlasResults")?.textContent.includes( + "Synthetic atlas test protein"),{timeout:25000}); + await page.click(".ram-atlas-verify"); + await page.waitForFunction(() => + document.querySelector("#atlasResults")?.textContent.includes( + "Verified exact SIFTS: 40 distinct PDB residues"), + {timeout:25000}); // Results are not permitted to act as conformation-cluster labels. assert.doesNotMatch(visible,/experimental state [1-9]/i); assert.ok(!errors.length,"Atlas browser JavaScript errors: "+errors.join(" | ")); From 3bc71c2251b4a5d93558c08196f0133cc54f2887 Mon Sep 17 00:00:00 2001 From: spomlol <77454330+CedricHermansBIT@users.noreply.github.com> Date: Fri, 9 Oct 2026 18:45:00 +0200 Subject: [PATCH 5/5] Preserve known-good PDBe connectivity fixture, simulate empty SIFTS only in crash test --- tests/atlas-browser.cjs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/atlas-browser.cjs b/tests/atlas-browser.cjs index 8bba4bac..9d3f2b6c 100644 --- a/tests/atlas-browser.cjs +++ b/tests/atlas-browser.cjs @@ -81,6 +81,9 @@ ${atoms}${component} await page.setViewport({width:1366,height:900}); const requests = []; const errors = []; + // The connectivity diagnostic deliberately checks known-good 4AKE. + // Switch to missing exact SIFTS only in the later crash regression. + let simulateMissingExactSifts = false; page.on("pageerror", error => errors.push(String(error.message || error))); await page.setRequestInterception(true); page.on("request", request => { @@ -118,7 +121,8 @@ ${atoms}${component} } else if (url === "https://www.ebi.ac.uk/pdbe/static/entry/4ake_updated.cif") { request.respond({status:200, headers:{...cors,"content-type":"text/plain"}, - body:syntheticUpdatedCif(101,false).replace(/P00533/g,"P00000")}); + body:syntheticUpdatedCif(101,false).replace(/P00533/g, + simulateMissingExactSifts ? "P00000" : "P69441")}); } else if (url === "https://www.ebi.ac.uk/pdbe/static/entry/1crn_updated.cif") { const mockCif = syntheticUpdatedCif(101,false); request.respond({status:200, @@ -444,6 +448,7 @@ ${atoms}${component} // An RCSB-linked polymer entity can have NO matching exact PDBe // SIFTS rows. Its card must explain the limitation without crashing // the Shiny observer, and a subsequent valid verification must work. + simulateMissingExactSifts = true; await page.setViewport({width:1366,height:900}); await page.$eval("#atlasAccession",element=>{ element.value="P69441";