[Bug] Session flagged when processing very simple R (bioinformatics data conversion) script

Open 💬 2 comments Opened Jun 11, 2026 by ndaniel

Fable 5 safety classifier false-positive for bioinformatics

Bug Description
Telling to Fable 5 (Claude Code) to read one simple R script that converts data between different bioinformatic data types, makes it to flag the session and switch to Opus 4.8.

Environment Info

  • Platform: linux
  • Terminal: gnome-terminal
  • Version: 2.1.173
  • Feedback ID: 786f037f-4e36-4373-a4eb-9a141f36082b

Command to Claude Code (Fable 5)

read the R sript                                                                                                                                                                                                                                       

Errors

This model has measures that flagged something in this session. This sometimes happens with safe, normal conversations. These measures let us bring you Mythos-level capability in other areas sooner, and we're working to refine them. Switched to 
  Opus 4.8. Send feedback with /feedback or learn more

R script

#!/usr/bin/env Rscript
# =============================================================================
# 01--export_to_anndata.R   (zeus, task T0.1)
#
# Export the latest delivery (data/) to a single AnnData .h5ad for the
# downstream squidpy neighbourhood analysis.
#
# Verified upstream (do not re-derive): rownames(metadata) == colnames(linear)
#   == metadata$seurat_cell_id, bit-identical order. So the two files assemble
#   directly with NO key-matching.
#
# Output:
#   - results01/zeus_cohort.h5ad
#       X            = linear intensities (cells x 29 features)
#       obs          = full 92-col metadata + *_clean label columns
#       var          = 29 features + channel_type (protein / Hoechst)
#       obsm['spatial'] = centroids in MICRONS (px * 0.325)
#       uns          = pixel size, provenance
#
# Notes:
#   - Pixel size 0.325 um/px is from Samuel (consistent across all 7 TMA slides).
#   - Area / Eccentricity are from Samuel (Centroids email, 3 Jun ); Area units
#     unstated -> assumed px^2. Segmentation QC (T0.1b) is left to the squidpy side.
# =============================================================================

suppressMessages({
  library(qs2)
  library(Matrix)
  library(SingleCellExperiment)
  library(zellkonverter)
})

PX_UM    <- 0.325                       # microns per pixel (Samuel)
DATA_DIR <- "data/0604"
OUT_DIR  <- "results01"
OUT_H5AD <- file.path(OUT_DIR, "zeus_cohort.h5ad")

META_QS   <- file.path(DATA_DIR, "0523_Metadata_with_centroids.qs2")
LINEAR_QS <- file.path(DATA_DIR, "Linear_Intensity_Values_Merged_Cohort.qs2")

dir.create(OUT_DIR, recursive = TRUE, showWarnings = FALSE)

# ----------------------------------------------------------------------------
# 1. Load
# ----------------------------------------------------------------------------
message("Loading metadata: ", META_QS)
meta <- qs_read(META_QS)
stopifnot(is.data.frame(meta))

message("Loading linear intensities: ", LINEAR_QS)
linear <- qs_read(LINEAR_QS)                 # features x cells (dgCMatrix)
stopifnot(nrow(linear) == 29)

# ----------------------------------------------------------------------------
# 2. Assert alignment (cells in identical order) -- hard fail if not
# ----------------------------------------------------------------------------
if (!identical(rownames(meta), colnames(linear))) {
  stop("Cell order mismatch between metadata rownames and linear-intensity colnames; ",
       "aborting (the upstream invariant no longer holds).")
}
message("Alignment OK: ", format(nrow(meta), big.mark = ","), " cells, identical order.")

# ----------------------------------------------------------------------------
# 3. Fix upstream TRIPOD typos IN PLACE, + harmonise Stroma labels to ASCII
#    Typos are objective spelling errors and are corrected in the actual
#    annotation columns so downstream uses clean labels by default. The raw
#    (typo'd) labels remain in the source qs2 under data/0604/, so nothing
#    is lost and provenance is intact.
#      Machrophages                      -> Macrophages
#      Proliferative_cancerous_epthelium -> Proliferative_cancerous_epithelium
# ----------------------------------------------------------------------------
fix_typos <- function(x) {
  x <- gsub("Machrophages", "Macrophages", x, fixed = TRUE)
  x <- gsub("Proliferative_cancerous_epthelium",
            "Proliferative_cancerous_epithelium", x, fixed = TRUE)
  x
}

ann_cols <- c("Tumor_Annotation", "Global",
              "Immune_Wide_Annotation", "Immune_Deep_Annotation",
              "Immune_Active_Annotation", "Immune_Checkpoint_Annotation",
              "Stroma_Annotation")
n_fixed <- 0L
for (col in ann_cols) {
  if (col %in% names(meta)) {
    before <- as.character(meta[[col]])
    after  <- fix_typos(before)
    n_fixed <- n_fixed + sum(before != after, na.rm = TRUE)
    meta[[col]] <- after
  }
}
message("Typos fixed in place: ", n_fixed, " cell labels across annotation columns.")

# Stroma_Annotation: Greek alpha + literal '+' are NOT typos but break R
# formulas / filenames -> keep the (typo-fixed) original AND add an ASCII copy
# matching Tumor_Annotation's encoding (a / underscores, no '+').
ascii_stroma <- function(x) {
  x <- gsub("α", "a", x)        # Greek small alpha -> a
  x <- gsub("+", "", x, fixed = TRUE)
  x <- gsub("__+", "_", x)      # collapse any doubled underscores
  x
}
if ("Stroma_Annotation" %in% names(meta))
  meta[["Stroma_Annotation_clean"]] <- ascii_stroma(as.character(meta[["Stroma_Annotation"]]))

# ----------------------------------------------------------------------------
# 4. colData hygiene: AnnData/HDF5 cannot store Date/POSIXct -> coerce to character
# ----------------------------------------------------------------------------
for (col in names(meta)) {
  if (inherits(meta[[col]], c("Date", "POSIXct", "POSIXt")))
    meta[[col]] <- as.character(meta[[col]])
}

# ----------------------------------------------------------------------------
# 5. Spatial coordinates in microns -> reducedDim "spatial" (becomes obsm['spatial'])
#    Keep raw px in obs (X_centroid/Y_centroid) and add explicit *_um as backup.
# ----------------------------------------------------------------------------
stopifnot(all(c("X_centroid", "Y_centroid") %in% names(meta)))
spatial_um <- cbind(X = meta$X_centroid * PX_UM,
                    Y = meta$Y_centroid * PX_UM)
rownames(spatial_um) <- rownames(meta)
meta$X_um <- spatial_um[, "X"]
meta$Y_um <- spatial_um[, "Y"]

# ----------------------------------------------------------------------------
# 6. var: 29 features + channel_type
# ----------------------------------------------------------------------------
feat <- rownames(linear)
rowdata <- DataFrame(
  feature      = feat,
  channel_type = ifelse(grepl("^Hoechst", feat), "Hoechst", "protein"),
  is_protein   = !grepl("^Hoechst", feat),
  row.names    = feat
)

# ----------------------------------------------------------------------------
# 7. Assemble SingleCellExperiment
# ----------------------------------------------------------------------------
sce <- SingleCellExperiment(
  assays  = list(linear = linear),       # features x cells; X_name = "linear"
  colData = DataFrame(meta, check.names = FALSE),
  rowData = rowdata
)
reducedDim(sce, "spatial") <- spatial_um
metadata(sce) <- list(
  pixel_size_um = PX_UM,
  spatial_units = "micron",
  X_layer       = "linear",
  source_meta   = basename(META_QS),
  source_linear = basename(LINEAR_QS),
  built         = as.character(Sys.time()),
  note          = "X = linear (un-logged) intensities; spatial in microns; TRIPOD typos fixed in place (Machrophages, epthelium); Stroma_Annotation_clean = ASCII"
)

message("SCE assembled: ", nrow(sce), " features x ", ncol(sce), " cells; ",
        ncol(colData(sce)), " obs columns.")

# ----------------------------------------------------------------------------
# 8. Write .h5ad  (native R HDF5 writer first; fall back to basilisk if needed)
# ----------------------------------------------------------------------------
message("Writing ", OUT_H5AD, " ...")
ok <- tryCatch({
  writeH5AD(sce, OUT_H5AD, X_name = "linear", use_native = TRUE, verbose = TRUE)
  TRUE
}, error = function(e) {
  message("  native writer failed (", conditionMessage(e), "); retrying via basilisk")
  writeH5AD(sce, OUT_H5AD, X_name = "linear")
  TRUE
})
stopifnot(isTRUE(ok))

message("DONE -> ", OUT_H5AD,
        " (", round(file.info(OUT_H5AD)$size / 1e6, 1), " MB)")
message("In Python: adata.X = linear intensities, adata.obsm['spatial'] in microns, ",
        "library key = 'All_ID'.")
        
        
  ```
        

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗