diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md
index 27bd9cec..3d272557 100644
--- a/.claude/CLAUDE.md
+++ b/.claude/CLAUDE.md
@@ -231,3 +231,36 @@ After `runAnalysis()`, check:
- Use `createJaspTable()`, `createJaspPlot()`, `createJaspHtml()` for output elements
- Always set `$dependOn()` for proper caching and state management
- Use containers for grouping related elements, state objects for reusing computed results
+
+## Container Environment (overrides Session Setup section above)
+
+This is a **headless Docker container**. There is no interactive user.
+
+### MANDATORY FIRST ACTION — R Session Connection
+
+Before doing ANYTHING else (before reading code, before exploring the repo),
+you MUST connect to the pre-started R session:
+
+1. Call `list_r_sessions` to discover available sessions.
+2. Call `select_r_session` to connect to the listed session.
+3. Only then proceed with the workflow.
+
+If `list_r_sessions` returns empty, the background R session failed to start.
+Fall back to Bash for R execution. Do NOT spend more than 2 turns debugging.
+
+This step is NOT optional. Without it, `btw_tool_run_r` will fail.
+
+### Other container rules
+
+- `session_startup.R` is NOT relevant here — ignore references to it.
+- Do NOT run any git commands (the outer orchestrator handles git).
+
+## Compact Instructions
+
+When context is compacted, preserve:
+- Current workflow stage (from /workspace/.openclaw-run/current_stage)
+- Implementation plan (from /workspace/.openclaw-run/PLAN.md)
+- The module name and original task description
+- What git changes have been made so far
+
+After compaction, re-read /workspace/.openclaw-run/RECOVERY.md to re-orient.
diff --git a/R/TimeWeightedCharts.R b/R/TimeWeightedCharts.R
index 153f9a00..22ae8c1e 100644
--- a/R/TimeWeightedCharts.R
+++ b/R/TimeWeightedCharts.R
@@ -194,10 +194,21 @@ timeWeightedCharts <- function(jaspResults, dataset, options) {
columnsToPass <- c(measurements, stages)
columnsToPass <- columnsToPass[columnsToPass != ""]
phase2 <- (options[["cumulativeSumChartSdSource"]] == "historical")
+ movingRangeLength <- options[["cumulativeSumChartAverageMovingRangeLength"]]
+ if (!phase2 && .stageHasTooFewObservationsForMovingRange(dataset, stages, movingRangeLength)) {
+ plot$setError(.timeWeightedStagesMovingRangeErrorMessage(
+ chartTitle = gettext("Cumulative sum chart"),
+ dataset = dataset,
+ stages = stages,
+ movingRangeLength = movingRangeLength
+ ))
+ return(list("plot" = plot))
+ }
+
cusumChart <- .controlChart(dataset[columnsToPass], plotType = "cusum", stages = stages, xBarSdType = options[["cumulativeSumChartSdMethod"]],
nSigmasControlLimits = options[["cumulativeSumChartNumberSd"]], xAxisLabels = axisLabels,
cusumShiftSize = options[["cumulativeSumChartShiftSize"]], cusumTarget = options[["cumulativeSumChartTarget"]],
- movingRangeLength = options[["cumulativeSumChartAverageMovingRangeLength"]], phase2 = phase2,
+ movingRangeLength = movingRangeLength, phase2 = phase2,
phase2Sd = options[["cumulativeSumChartSdValue"]], tableLabels = axisLabels, ruleList = ruleList)
table <- cusumChart$table
plot$plotObject <- cusumChart$plotObject
@@ -221,9 +232,20 @@ timeWeightedCharts <- function(jaspResults, dataset, options) {
columnsToPass <- c(measurements, stages)
columnsToPass <- columnsToPass[columnsToPass != ""]
phase2 <- (options[["exponentiallyWeightedMovingAverageChartSdSource"]] == "historical")
+ movingRangeLength <- options[["exponentiallyWeightedMovingAverageChartMovingRangeLength"]]
+ if (!phase2 && .stageHasTooFewObservationsForMovingRange(dataset, stages, movingRangeLength)) {
+ plot$setError(.timeWeightedStagesMovingRangeErrorMessage(
+ chartTitle = gettext("Exponentially weighted moving average chart"),
+ dataset = dataset,
+ stages = stages,
+ movingRangeLength = movingRangeLength
+ ))
+ return(list("plot" = plot))
+ }
+
ewmaChart <- .controlChart(dataset[columnsToPass], plotType = "ewma", stages = stages, xBarSdType = options[["exponentiallyWeightedMovingAverageChartSdMethod"]],
nSigmasControlLimits = options[["exponentiallyWeightedMovingAverageChartSigmaControlLimits"]],
- xAxisLabels = axisLabels, movingRangeLength = options[["exponentiallyWeightedMovingAverageChartMovingRangeLength"]],
+ xAxisLabels = axisLabels, movingRangeLength = movingRangeLength,
ewmaLambda = options[["exponentiallyWeightedMovingAverageChartLambda"]], phase2 = phase2,
phase2Sd = options[["exponentiallyWeightedMovingAverageChartSdValue"]], tableLabels = axisLabels,
ruleList = ruleList)
@@ -247,3 +269,20 @@ timeWeightedCharts <- function(jaspResults, dataset, options) {
return(ruleList)
}
+.stageHasTooFewObservationsForMovingRange <- function(dataset, stages, movingRangeLength) {
+ if (identical(stages, "") || !stages %in% colnames(dataset))
+ return(FALSE)
+
+ stageCounts <- table(dataset[[stages]])
+ any(stageCounts < movingRangeLength)
+}
+
+.timeWeightedStagesMovingRangeErrorMessage <- function(chartTitle, dataset, stages, movingRangeLength) {
+ stageCounts <- table(dataset[[stages]])
+ minObservations <- min(stageCounts)
+ gettextf(paste0(
+ "At least one stage contains only %1$s observation(s). %2$s requires at least %3$s observations per stage ",
+ "for the selected moving range length. This can happen when too many stages are defined (for example one point ",
+ "per stage). Reduce the number of stages or lower the moving range length."
+ ), minObservations, chartTitle, movingRangeLength)
+}
diff --git a/tests/testthat/_snaps/example-Type1InstrumentCapability/analysis-1-figure-1-bias-histogram.new.svg b/tests/testthat/_snaps/example-Type1InstrumentCapability/analysis-1-figure-1-bias-histogram.new.svg
new file mode 100644
index 00000000..0b696f60
--- /dev/null
+++ b/tests/testthat/_snaps/example-Type1InstrumentCapability/analysis-1-figure-1-bias-histogram.new.svg
@@ -0,0 +1,253 @@
+
+
diff --git a/tests/testthat/_snaps/msaType1Gauge/1-bias-histogram.new.svg b/tests/testthat/_snaps/msaType1Gauge/1-bias-histogram.new.svg
new file mode 100644
index 00000000..f28037c6
--- /dev/null
+++ b/tests/testthat/_snaps/msaType1Gauge/1-bias-histogram.new.svg
@@ -0,0 +1,253 @@
+
+
diff --git a/tests/testthat/_snaps/msaType1Gauge/2-bias-histogram.new.svg b/tests/testthat/_snaps/msaType1Gauge/2-bias-histogram.new.svg
new file mode 100644
index 00000000..0e215eaf
--- /dev/null
+++ b/tests/testthat/_snaps/msaType1Gauge/2-bias-histogram.new.svg
@@ -0,0 +1,247 @@
+
+
diff --git a/tests/testthat/_snaps/msaType1Gauge/3-bias-histogram.new.svg b/tests/testthat/_snaps/msaType1Gauge/3-bias-histogram.new.svg
new file mode 100644
index 00000000..e07d4ebb
--- /dev/null
+++ b/tests/testthat/_snaps/msaType1Gauge/3-bias-histogram.new.svg
@@ -0,0 +1,248 @@
+
+
diff --git a/tests/testthat/_snaps/msaType1Gauge/4-bias-histogram.new.svg b/tests/testthat/_snaps/msaType1Gauge/4-bias-histogram.new.svg
new file mode 100644
index 00000000..3c9b9602
--- /dev/null
+++ b/tests/testthat/_snaps/msaType1Gauge/4-bias-histogram.new.svg
@@ -0,0 +1,176 @@
+
+