From 3536e7d7fb2ab6cdb24d0f90205d1846d1c7b993 Mon Sep 17 00:00:00 2001 From: Julius Pfadt Date: Thu, 6 Aug 2026 09:47:27 +0200 Subject: [PATCH 01/14] chore: scaffold jaspBayesianQualityControl from module template strip template example analyses, reuse quality control icons, set module metadata and Description.qml Co-Authored-By: Claude Opus 5 (1M context) --- DESCRIPTION | 29 ++- NAMESPACE | 13 +- R/addOne.R | 8 - R/interface.R | 47 ----- R/loadingData.R | 30 --- R/parabola.R | 21 -- inst/Description.qml | 64 ++---- inst/icons/exampleIcon.png | Bin 1820 -> 0 bytes inst/icons/exampleIcon.svg | 83 -------- inst/icons/qualityControl-capability.svg | 95 +++++++++ inst/icons/qualityControl-measurement.svg | 175 ++++++++++++++++ inst/icons/qualityControl-module.svg | 110 ++++++++++ inst/qml/AddOne.qml | 60 ------ inst/qml/Interface.qml | 188 ------------------ inst/qml/LoadingData.qml | 113 ----------- inst/qml/Parabola.qml | 47 ----- ....Rproj => jaspBayesianQualityControl.Rproj | 0 17 files changed, 421 insertions(+), 662 deletions(-) delete mode 100644 R/addOne.R delete mode 100644 R/interface.R delete mode 100644 R/loadingData.R delete mode 100644 R/parabola.R delete mode 100644 inst/icons/exampleIcon.png delete mode 100644 inst/icons/exampleIcon.svg create mode 100644 inst/icons/qualityControl-capability.svg create mode 100644 inst/icons/qualityControl-measurement.svg create mode 100644 inst/icons/qualityControl-module.svg delete mode 100644 inst/qml/AddOne.qml delete mode 100644 inst/qml/Interface.qml delete mode 100644 inst/qml/LoadingData.qml delete mode 100644 inst/qml/Parabola.qml rename jaspModule.Rproj => jaspBayesianQualityControl.Rproj (100%) diff --git a/DESCRIPTION b/DESCRIPTION index 1c65efc..d19b06b 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,19 +1,28 @@ -Package: jaspModuleTemplate -Type: Package -Title: A module for JASP -Version: 0.1.2 -Date: 2020-10-15 +Package: jaspBayesianQualityControl +Type: Package +Title: Bayesian Quality Control Module for JASP +Version: 0.1.0 +Date: 2026-08-06 Author: JASP Team -Website: jasp-stats.org -Maintainer: JASP Team -Description: Example module showing basic functionality. Use it for inspiration when creating your own module. +Website: https://github.com/jasp-stats/jaspBayesianQualityControl +Maintainer: JASP +Description: Bayesian counterparts to the quality control analyses, covering process capability and measurement systems analysis. License: GPL (>= 2) Encoding: UTF-8 Imports: + BayesTools, + ggh4x, + ggplot2, + HDInterval, jaspBase, - jaspGraphs + jaspGraphs, + qc, + tibble Suggests: testthat Remotes: jasp-stats/jaspBase, - jasp-stats/jaspGraphs + jasp-stats/jaspGraphs, + FBartos/qc +Roxygen: list(markdown = TRUE) +RoxygenNote: 7.3.3 diff --git a/NAMESPACE b/NAMESPACE index 316fe98..de0807f 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,5 +1,8 @@ -import(jaspBase) -export(addOne) -export(processTable) -export(parabola) -export(interfaceExample) +# Generated by roxygen2: do not edit by hand + +export(bayesianProcessCapabilityStudies) +importFrom(jaspBase,"%setOrRetrieve%") +importFrom(jaspBase,createJaspPlot) +importFrom(jaspBase,createJaspState) +importFrom(jaspBase,createJaspTable) +importFrom(jaspBase,jaspDeps) diff --git a/R/addOne.R b/R/addOne.R deleted file mode 100644 index 0fbc9fd..0000000 --- a/R/addOne.R +++ /dev/null @@ -1,8 +0,0 @@ -addOne <- function(jaspResults, dataset, options) { - result <- as.character(options$my_number + 1) # options$my_number comes from the menu created by inst/qml/integer.qml - - jaspResults[["result"]] <- createJaspHtml(text = result, - title = "This is your result:") - - return() -} diff --git a/R/interface.R b/R/interface.R deleted file mode 100644 index 99d5240..0000000 --- a/R/interface.R +++ /dev/null @@ -1,47 +0,0 @@ -interfaceExample <- function(jaspResults, dataset, options) { - # Just show the options as they are understood the R backend - jaspResults[["explanation"]] <- createJaspHtml(title = "User inputs, returned as html", - text = sprintf("Here we show, for pedagogical purposes, the user inputs as they are understood by the R backend.")) - - jaspResults[["logicals"]] <- createJaspHtml( - title = "Logical controls", - text = sprintf("The tick mark is set to: %s - The radio buttons are set to: %s", - as.character(options$my_tick_mark), # These variables are defined in .inst/qml/Interface.qml - as.character(options$radio_buttons)) # Notice we have to be careful with the data type - ) - - jaspResults[["others"]] <- createJaspHtml( - title = "Other controls", - text = sprintf("The chosen dropdown element is: %s - The slider value is: %s", - as.character(options$my_dropdown), - as.character(options$my_slider)) - ) - - jaspResults[["keyboard"]] <- createJaspHtml( - title = "Keyboard controls", - text = sprintf("The integer is set to: %s - The double is set to: %s - The percentage is set to: %s - The confidence interval is set to: %s - The text box is set to: %s", - as.character(options$my_integer), - as.character(options$my_double), - as.character(options$my_percent), - as.character(options$my_ci), - options$my_text) # No data-type conversion needed for text - ) - - jaspResults[["developers"]] <- createJaspHtml( - title = "Note for developers", - text = sprintf("Potential developers will find it useful to inspect the following files: - ") - ) - - return() -} diff --git a/R/loadingData.R b/R/loadingData.R deleted file mode 100644 index 3427eda..0000000 --- a/R/loadingData.R +++ /dev/null @@ -1,30 +0,0 @@ -processTable <- function(jaspResults, dataset, options) { - - # Auxiliary function. - # Returns TRUE if and only if an option has been assigned in the GUI - .isAssigned <- function(option) { - not_assigned <- as.character(option) == "" - return(!not_assigned) - } - - # Only if everything has been assigned ... - if(.isAssigned(options$ts) && .isAssigned(options$xs)) { - # ... print the inputs as a table - stats <- createJaspTable(gettext("Some descriptives")) - stats$dependOn(c("ts", "xs")) # Declare dependencies to make the object disappear / reappear when needed - - stats$addColumnInfo(name = gettext("times")) - stats$addColumnInfo(name = gettext("xs")) - - stats[["times"]] <- dataset[[options$ts]] - stats[["xs"]] <- dataset[[options$xs]] - - jaspResults[["stats"]] <- stats - } else { - expl <- createJaspHtml(text = "Select times and positions") - expl$dependOn(c("ts", "xs")) # Declare dependencies to make the object disappear / reappear when needed - - jaspResults[["Explanation"]] <- expl - } - -} diff --git a/R/parabola.R b/R/parabola.R deleted file mode 100644 index 3c6019a..0000000 --- a/R/parabola.R +++ /dev/null @@ -1,21 +0,0 @@ -parabola <- function(jaspResults, dataset, options) { - # Analysis - f <- function(x) { options$a * x^2 } # Function to be plotted - p <- ggplot2::ggplot() + # Plotting command - ggplot2::xlim(-3, 3) + - ggplot2::ylim(0, 10) + - ggplot2::geom_function(fun = f) - # add jasp theme - p <- p + jaspGraphs::geom_rangeframe() + - jaspGraphs::themeJaspRaw() - # Aesthetics - parabolaPlot <- createJaspPlot(title = gettext("Parabola"), - width = 160, - height = 320) - parabolaPlot$dependOn(c("a")) # Refresh view whenever a changes - parabolaPlot$info <- gettext("This figure displays a parabola specified via the `a` option.") - jaspResults[["parabolaPlot"]] <- parabolaPlot - parabolaPlot$plotObject <- p - - return() -} diff --git a/inst/Description.qml b/inst/Description.qml index a86a4b7..af7bf6b 100644 --- a/inst/Description.qml +++ b/inst/Description.qml @@ -3,64 +3,28 @@ import JASP.Module Description { - name : "jaspModuleTemplate" - title : qsTr("Jasp Module") - description : qsTr("Examples for module builders") - version : "0.1" + name : "jaspBayesianQualityControl" + title : qsTr("Bayesian Quality Control") + description : qsTr("Bayesian analyses for investigating whether a manufactured product adheres to a defined set of quality criteria") + version : "0.1.0" author : "JASP Team" - maintainer : "JASP Team " - website : "https://jasp-stats.org" + maintainer : "JASP " + website : "https://github.com/jasp-stats/jaspBayesianQualityControl" license : "GPL (>= 2)" - icon : "exampleIcon.png" // Located in /inst/icons/ - preloadData: true - requiresData: true + icon : "qualityControl-module.svg" + hasWrappers : false + preloadData : false GroupTitle { - title: qsTr("Basic interactivity") + title: qsTr("Capability Analysis") + icon: "qualityControl-capability.svg" } Analysis { - title: qsTr("Using the interface") // Title for window - menu: qsTr("Using the interface") // Title for ribbon - func: "interfaceExample" // Function to be called - qml: "Interface.qml" // Design input window - requiresData: false // Allow to run even without data - } - - Analysis - { - title: qsTr("Loading data") - menu: qsTr("Loading data") - func: "processTable" - qml: "LoadingData.qml" - } - - GroupTitle - { - title: qsTr("Basic functions") - } - - Analysis - { - title: qsTr("Add one") // Title for window - menu: qsTr("Add one") // Title for ribbon - func: "addOne" // Function to be called - qml: "AddOne.qml" // Design input window - requiresData: false // Allow to run even without data - } - - GroupTitle - { - title: qsTr("Plotting") - } - - Analysis - { - title: qsTr("Plot a parabola") - func: "parabola" - qml: "Parabola.qml" - requiresData: false + title: qsTr("Bayesian Process Capability Study") + func: "bayesianProcessCapabilityStudies" + preloadData: true } } diff --git a/inst/icons/exampleIcon.png b/inst/icons/exampleIcon.png deleted file mode 100644 index 09c807f03517311bb34af9e7405f98566e996658..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1820 zcmV+%2jlpOP)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H12B}Fz zK~#90?V8zh+sF}zzXorSlw`>(OI~|zSGE$TvXy<>-9IVMdCz11yxb2-Z0}L)l_=}J zz?(qlp#g#b0U8upWxIl}iY1Vk82~@@^mO+K3JMAeK3*g|@czUfJmHUk_W|$cqd^q6 z@@Kvazn8;c`pa9sWy`~IPB6u>giiU+m(r<>BNz-1jBRFIK_mVKl?L3akw8buA=Dsopv4_pA$b6~APT{Mti zHlgC7&u{YeajVF?7F;yaZ>BYZ`UKo649ZaUVQYx)yYQ%+?;4j4R0^?Do;!KHSuhzaGR+>Bbno z=|tna-bD`E_ChWiaMVVBe`*h(hA;4H7ddWC$CIy5pfQ8jnK+pX-*P@`Sc3D09S_QIwy{)qwua!uZ$)ZeTPac)lS2+?$Go zqwv)k8WSm?n6EKsK7X}7`RpBDE^{!Kc7F- zUth*wSm2vXYdg#;J=zI8F>j*gI zs10A-nxC2d{iL;USzQ`sF7MLEZrqP~j;)}xZR(TvPwQEp-=JRvt9Iy4R-StOGF;Ws z0~h`LE`H7vBsnElKsxOx<`V2-zPyY#x~j)~C4kgY9QYziKD5jiiuZn@@`VS-ZP@9X z@7U|Yn+GwUp+}o$ZTDg1Act*qTtcqvCPLLi4_xHHjW6K=l9uV0mawKIL09o*h48>P zi_yn!&_k!uY>4#Bb5kKVo`Vj{Ff5~?C5=nSWdnWJgyFm>nsn^2l2962r>PZv%_d~d|6oa>0=*tV(k6w;QiYO}>)U4 zqm6{`P|%|_g$jwO4Z!zNbgpSZJ!+GJ9X(*Y^BU%!Ocv$ ztIogOWJ#yup}#weU;DTPZjJ@zx{my9--H&8?rCX@I^VDY*j5fR-kxtWmxqOnOLL_R zO;$dAL4J4;Z}a39{X8Y0-R;B6b9>mJFaLU!ov8bQyxp`S-jmx{SYj8->&$(DvnKK; zxY16_oM&^CD1>44sIIMwa|C8B&u=W07?t3*YL(Fh(ygGccVKI1TSo7i(SvRdEmU}z zJ^l5u3(Ya|YS$_XgEIW(!20a=(VAzu=yeS_ZCanN&f;T+lP0$_AC=&`hJH3P$aw?l zmC;0sBBxEXSGEdeN}2&!Vejd!S?77f3TllMNAHW}l8)K~Ji9?Qhv@~lG`5E5w;ed! zFflIc(=cXgBdrlUy|smDE)KI+n%ko46L@(s*CW{6+*#Oh3v9j zA(T;A)cPodg2a$C69mK}Yt# zMiOp}jvY|3-IfI6XZ&--IyY5wlRfp86DlZ_rRIKlFB|?d#MnVpY`@kRIeD5|&REzf`!PR%t zx4{CYLX|xJqb`3(SK1-_M2M=kvk$8P!3K{9iGBQnf`WpNm;V9d)R%Pjsfz~y0000< KMNUMnLSTX~@rLLC diff --git a/inst/icons/exampleIcon.svg b/inst/icons/exampleIcon.svg deleted file mode 100644 index 1399625..0000000 --- a/inst/icons/exampleIcon.svg +++ /dev/null @@ -1,83 +0,0 @@ - - - - - - - - - - - - - - - -ABC diff --git a/inst/icons/qualityControl-capability.svg b/inst/icons/qualityControl-capability.svg new file mode 100644 index 0000000..1ccbbc3 --- /dev/null +++ b/inst/icons/qualityControl-capability.svg @@ -0,0 +1,95 @@ + + + +image/svg+xml + + + + + + + + + + \ No newline at end of file diff --git a/inst/icons/qualityControl-measurement.svg b/inst/icons/qualityControl-measurement.svg new file mode 100644 index 0000000..2aa5d3d --- /dev/null +++ b/inst/icons/qualityControl-measurement.svg @@ -0,0 +1,175 @@ + + + +image/svg+xml + + + + + + + + + + \ No newline at end of file diff --git a/inst/icons/qualityControl-module.svg b/inst/icons/qualityControl-module.svg new file mode 100644 index 0000000..d649433 --- /dev/null +++ b/inst/icons/qualityControl-module.svg @@ -0,0 +1,110 @@ + + + +image/svg+xml + + + + + + + + + + \ No newline at end of file diff --git a/inst/qml/AddOne.qml b/inst/qml/AddOne.qml deleted file mode 100644 index 8566f84..0000000 --- a/inst/qml/AddOne.qml +++ /dev/null @@ -1,60 +0,0 @@ -// -// Copyright (C) 2013-2018 University of Amsterdam -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public -// License along with this program. If not, see -// . -// -import QtQuick -import QtQuick.Layouts -import JASP.Controls -import JASP.Widgets -import JASP - -Form -{ - - info: qsTr("This analysis just adds one to the input. \ - It's purpose is to show an easy example of how a manual input gets processed \ - by the backend, and returned. - \\ - \\ - From the technical point of view, the most challenging part of JASP module \ - development is the communication between the QML interface and the R backend. - \\ - \\ - Playing with the current JASP analysis while simultaneously inspecting the R \ - code in the files `./inst/qml/AddOne.qml` and `./R/examples.R` is a good \ - way to learn how this communication works. - ") - - Text - { - text: qsTr("This example shows how to manually introduce an input and perform a simple operation on it") - } - - IntegerField - { - info: qsTr("This is the number that will be used in the operation") - - name: "my_number" // This will map to options$my_number in R - label: qsTr("Type a number") // qsTr allows for future translations - - // We can add some extra control parameters - min: 1 - defaultValue: 10 - fieldWidth: 50 - max: 1000 - } - -} diff --git a/inst/qml/Interface.qml b/inst/qml/Interface.qml deleted file mode 100644 index da8154c..0000000 --- a/inst/qml/Interface.qml +++ /dev/null @@ -1,188 +0,0 @@ -// -// Copyright (C) 2013-2018 University of Amsterdam -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public -// License along with this program. If not, see -// . -// -import QtQuick -import QtQuick.Layouts -import JASP.Controls -import JASP.Widgets -import JASP - -Form -{ - - info: qsTr("This analysis shows you different interface elements of JASP, such as tick marks, text boxes, ... \\ - Its purpose is pedagogical, and its target audience is that of JASP module developers. \\ - \\ - From the technical point of view, the most challenging part of JASP module development is the communication between the QML interface and the R backend. \\ - Playing with the current JASP analysis while simultaneously inspecting the R code in the files `./inst/qml/Interface.qml` and `./R/examples.R` is a good way to learn how this communication works. \\ - \\ - The source code is available at [github.com/jasp-stats/jaspModuleTemplate](https://github.com/jasp-stats/jaspModuleTemplate)") - - Text - { - text: qsTr("This analysis shows you different interface elements of JASP") - // The qsTr wrapper allows for future translations. As a rule of thumb, you should always use qsTr for any text that will be displayed to the user. - } - - Group - { - title: qsTr("Logical controls") - - CheckBox - { - info: qsTr("This is a tick mark that can be used to control the flow of the analysis") - - name: "my_tick_mark" - label: qsTr("Tick mark") - - // We can add some extra control parameters - checked: false // Default value - } - - RadioButtonGroup - { - name: "radio_buttons" - title: qsTr("Radio buttons") - - RadioButton { value: "one value"; label: qsTr("One"); checked: true } // Single-line definition is also possible - RadioButton { value: "another value"; label: qsTr("Another") } - } - } - - Group - { - title: qsTr("Other controls") - - DropDown - { - info: qsTr("This is a dropdown that can be used to select one of a list of options") - - name: "my_dropdown" - label: qsTr("Select an option") - - // We can add some extra control parameters - values: ["option 1", "option 2", "option 3"] - } - - Slider - { - - info: qsTr("This is a slider that can be used to select a value in a range") - - name: "my_slider" - label: qsTr("Select a value") - - // We can add some extra control parameters - min: 0 - max: 1 - value: 0.5 - decimals: 3 - vertical: false - } - } - - Group - { - title: qsTr("Keyboard inputs") - - IntegerField - { - info: qsTr("This is the number that will be used in the operation") - - name: "my_integer" // This will map to options$my_integer in R - label: qsTr("Input an integer") // qsTr allows for future translations - - // We can add some extra control parameters - min: 1 - defaultValue: 10 - fieldWidth: 50 - max: 1000 - } - - DoubleField - { - info: qsTr("This is the number that will be used in the operation") - - name: "my_double" - label: qsTr("Input a number with decimals") - - // We can add some extra control parameters - defaultValue: 3.14 - fieldWidth: 50 - max: 5 - decimals: 2 - } - - PercentField - { - info: qsTr("This is the number that will be used in the operation") - - name: "my_percent" - label: qsTr("Input a percentage") - } - - CIField - { - info: qsTr("This is the number that will be used in the operation") - - name: "my_ci" - label: qsTr("Input a confidence interval") - } - - TextField - { - info: qsTr("This is a text field that can be used to input any text") - - name: "my_text" - label: qsTr("Input some text") - - // We can add some extra control parameters - fieldWidth: 200 - defaultValue: qsTr("Hello world!") - } - } - - Section - { - title: qsTr("Advanced controls") - - - Group - { - title: qsTr("Subordinate menus") - - CheckBox - { - - name: "my_advanced_tick_mark" - label: qsTr("Activate advanced options?") - - // We can add some extra control parameters - checked: false // Default value - - // The tic mark below is only available if the above tick mark is checked - CheckBox - { - name: "my_subordinate_tick_mark" - label: qsTr("Subordinate tick mark") - checked: false // Default value - } - } - } - } - -} diff --git a/inst/qml/LoadingData.qml b/inst/qml/LoadingData.qml deleted file mode 100644 index c61caf0..0000000 --- a/inst/qml/LoadingData.qml +++ /dev/null @@ -1,113 +0,0 @@ -// -// Copyright (C) 2013-2018 University of Amsterdam -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public -// License along with this program. If not, see -// . -// -import QtQuick -import QtQuick.Layouts -import JASP.Controls -import JASP.Widgets -import JASP - -Form -{ - info: qsTr("This example shows how to load a dataset and output it as a table") - - Text - { - text: "This example shows how to load a dataset and output it as a table" - } - - VariablesForm - { - AvailableVariablesList { name: "allVariables" } - - AssignedVariablesList { - name: "ts" - label: qsTr("Times (t)") - info: qsTr("This info entry adds documentation to the (i) icon in the analysis file. E.g., Specify variable containing the time.") - singleVariable: true - allowedColumns: ["scale"] - } - - AssignedVariablesList { - name: "xs" - label: qsTr("Positions (x)") - info: qsTr("This info entry adds documentation to the (i) icon in the analysis file. E.g., Specify variable containing the positions.") - singleVariable: true - allowedColumns: ["scale"] - } - } - - Section - { - title : qsTr("Advanced") - columns: 1 - - Text { text: qsTr("This example shows how to get the factors of a variable") } - - DropDown - { - id : nominalOrOrdinalVariables - name : "nominalOrOrdinalVariables" - label : "Nominal or Ordinal variable" - addEmptyValue : true - placeholderText : qsTr("Select one variable") - source : [{isDataSetVariables: true, use: "type=nominal|ordinal" } ] - info : qsTr("Only nominal or ordinal variable are available. Choose one of them.") - } - - Text - { - text : qsTr("Warning: No nominal or ordinal variable in your dataset.
Either change a variable type from scale to nominal (or ordinal), or load another dataset") - visible : nominalOrOrdinalVariables.count === 1 // Empty value is already 1 element. - } - - Group - { - visible : nominalOrOrdinalVariables.value !== "" - - ComponentsList - { - id : valuePerLevel - title : "Set value for each factor" - name : "values" - source : nominalOrOrdinalVariables.value !== "" ? [{values: [nominalOrOrdinalVariables.value], use: "levels" }] : [] - headerLabels : [qsTr("Check"), qsTr("Value")] - - rowComponent: RowLayout - { - Text { text: rowValue ; Layout.preferredWidth: 100 * jaspTheme.uiScale } - CheckBox { name: "check" ; Layout.preferredWidth: 100 * jaspTheme.uiScale } - DoubleField { name: "double" } - } - } - - Text - { - text : qsTr("The factors checked in the list above will be present in the dropdown below") - } - - DropDown - { - label : "Checked factors" - name : "checkedFactor" - source : [{ name: "values", condition: "check"}] - addEmptyValue : true - } - } - } - -} diff --git a/inst/qml/Parabola.qml b/inst/qml/Parabola.qml deleted file mode 100644 index 416138c..0000000 --- a/inst/qml/Parabola.qml +++ /dev/null @@ -1,47 +0,0 @@ -// -// Copyright (C) 2013-2018 University of Amsterdam -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public -// License along with this program. If not, see -// . -// -import QtQuick -import QtQuick.Layouts -import JASP.Controls -import JASP.Widgets -import JASP - -Form -{ - info: qsTr("This example shows how to plot a curve.") - - Text - { - text: qsTr("This example shows how to plot a curve") - } - - IntegerField - { - info: qsTr("The shape of the parabola is dynamically determined by this number") - - name: "a" // This will map to options$a in R - label: qsTr("Type a number") // qsTr allows for future translations - - // We can add some extra control parameters - min: -10 - defaultValue: 1 - fieldWidth: 50 - max: 10 - } - -} diff --git a/jaspModule.Rproj b/jaspBayesianQualityControl.Rproj similarity index 100% rename from jaspModule.Rproj rename to jaspBayesianQualityControl.Rproj From 7784b18d5d24bd4b353c9fdf6195891488995718 Mon Sep 17 00:00:00 2001 From: Don van den Bergh Date: Thu, 6 Aug 2026 09:47:39 +0200 Subject: [PATCH 02/14] feat: add Bayesian process capability study ported verbatim from jasp-stats/jaspQualityControl#414 (branch vandenman:bqc, commit afe3837). dead common/Priors.qml dropped, common/PriorsNew.qml renamed to common/Priors.qml. --- R/bayesianProcessCapabilityStudies.R | 1001 +++++++++++++++++ inst/qml/bayesianProcessCapabilityStudies.qml | 463 ++++++++ inst/qml/common/PlotLayout.qml | 266 +++++ inst/qml/common/Priors.qml | 352 ++++++ 4 files changed, 2082 insertions(+) create mode 100644 R/bayesianProcessCapabilityStudies.R create mode 100644 inst/qml/bayesianProcessCapabilityStudies.qml create mode 100644 inst/qml/common/PlotLayout.qml create mode 100644 inst/qml/common/Priors.qml diff --git a/R/bayesianProcessCapabilityStudies.R b/R/bayesianProcessCapabilityStudies.R new file mode 100644 index 0000000..f344e81 --- /dev/null +++ b/R/bayesianProcessCapabilityStudies.R @@ -0,0 +1,1001 @@ +# +# Copyright (C) 2013-2025 University of Amsterdam +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +#'@importFrom jaspBase jaspDeps %setOrRetrieve% +#'@importFrom rlang .data + + +#'@export +bayesianProcessCapabilityStudies <- function(jaspResults, dataset, options) { + + fit <- .bpcsCapabilityTable(jaspResults, dataset, options, position = 1) + priorFit <- .bpcsSamplePosteriorOrPrior(jaspResults, dataset, options, prior = TRUE) + + .bpcsCapabilityPlot(jaspResults, options, fit, priorFit, position = 2) + .bpcsCapabilityPlot(jaspResults, options, fit, priorFit, position = 3, base = "priorDistributionPlot") + + .bpcsIntervalTable(jaspResults, options, fit, position = 4) + + .bpcsSequentialPointEstimatePlot( jaspResults, dataset, options, fit, position = 5) + .bpcsSequentialIntervalEstimatePlot(jaspResults, dataset, options, fit, position = 6) + + .bpcsPlotPredictive(jaspResults, dataset, options, fit, position = 7, base = "posteriorPredictiveDistributionPlot") + .bpcsPlotPredictive(jaspResults, dataset, options, priorFit, position = 8, base = "priorPredictiveDistributionPlot") + +} + +.bpcsIsReady <- function(options) { + # hasData <- if (options[["dataFormat"]] == "longFormat") { + # length(options[["measurementLongFormat"]]) > 0L && options[["measurementLongFormat"]] != "" + # } else { + # length(options[["measurementsWideFormat"]]) > 0L + # } + hasData <- length(options[["measurementLongFormat"]]) > 0L && options[["measurementLongFormat"]] != "" + hasData && + options[["lowerSpecificationLimit"]] && + options[["upperSpecificationLimit"]] && + options[["target"]] +} + +.bpcsStateDeps <- function() { + c( + # data + # "dataFormat", "measurementLongFormat", "measurementsWideFormat", + # "subgroupSizeType", "manualSubgroupSizeValue", "subgroup", "groupingVariableMethod", + # "stagesLongFormat", "stagesWideFormat", + "measurementLongFormat", + # specification + "target", "lowerSpecificationLimit", "upperSpecificationLimit", + "targetValue", "lowerSpecificationLimitValue", "upperSpecificationLimitValue", + # likelihood + "capabilityStudyType", + # prior + "priorSettings", "normalModelComponentsList", "tModelComponentsList", + # MCMC settings + "noIterations", "noWarmup", "noChains" + ) +} + +.bpcsDefaultDeps <- function() { + c( + .bpcsStateDeps(), + "axisLabels", + # metrics + "Cp", "Cpu", "Cpl", "Cpk", "Cpc", "Cpm" + ) +} + +.bpcsPlotLayoutDeps <- function(base, hasPrior = TRUE, hasEstimate = TRUE, hasCi = TRUE, hasType = FALSE, hasAxes = TRUE) { + c( + base, + if (hasEstimate) .bpcsPlotLayoutEstimateDeps(base), + if (hasCi) .bpcsPlotLayoutCiDeps(base), + if (hasType) .bpcsPlotLayoutTypeDeps(base), + if (hasAxes) .bpcsPlotLayoutAxesDeps(base), + if (hasPrior) .bpcsPlotLayoutPriorDeps(base) + ) +} + +.bpcsPlotLayoutEstimateDeps <- function(base) { paste0(base, c("IndividualPointEstimate", "IndividualPointEstimateType")) } +.bpcsPlotLayoutCiDeps <- function(base) { paste0(base, c("IndividualCi", "IndividualCiType", "IndividualCiMass", "IndividualCiLower", "IndividualCiUpper", "IndividualCiBf")) } +.bpcsPlotLayoutTypeDeps <- function(base) { paste0(base, c("TypeLower", "TypeUpper")) } +.bpcsPlotLayoutAxesDeps <- function(base) { paste0(base, c("PanelLayout", "Axes", "custom_x_min", "custom_x_max", "custom_y_min", "custom_y_max")) } +.bpcsPlotLayoutPriorDeps <- function(base) { paste0(base, "PriorDistribution") } + +.bpcsProcessCriteriaDeps <- function() { + c(paste0("interval", 1:4), paste0("intervalLabel", 1:5)) +} + +.bpcsPriorComponentByName <- function(options, name) { + components <- options$normalModelComponentsList + for (comp in components) { + if (comp$name == name) + return(comp) + } + return(NULL) +} + +.bpcsPriorFromComponent <- function(optionsPrior, paramName) { + if (is.null(optionsPrior)) + return(NULL) + + if (optionsPrior$type == "jeffreys") + return(paste0("Jeffreys_", paramName)) + + arguments <- list() + + arguments[["distribution"]] <- switch( + optionsPrior[["type"]], + "gammaAB" = "gamma", + "gammaK0" = "gamma", + optionsPrior[["type"]] + ) + + arguments[["parameters"]] <- switch( + optionsPrior[["type"]], + "normal" = list("mean" = optionsPrior[["mu"]], "sd" = optionsPrior[["sigma"]]), + "t" = list("location" = optionsPrior[["mu"]], "scale" = optionsPrior[["sigma"]], "df" = optionsPrior[["nu"]]), + "cauchy" = list("location" = optionsPrior[["mu"]], "scale" = optionsPrior[["theta"]]), + "gammaAB" = list("shape" = optionsPrior[["alpha"]], "rate" = optionsPrior[["beta"]]), + "gammaK0" = list("shape" = optionsPrior[["k"]], "rate" = 1/optionsPrior[["theta"]]), + "invgamma" = list("shape" = optionsPrior[["alpha"]], "scale" = optionsPrior[["beta"]]), + "lognormal" = list("meanlog" = optionsPrior[["mu"]], "sdlog" = optionsPrior[["sigma"]]), + "beta" = list("alpha" = optionsPrior[["alpha"]], "beta" = optionsPrior[["beta"]]), + "uniform" = list("a" = optionsPrior[["a"]], "b" = optionsPrior[["b"]]), + "exponential" = list("rate" = optionsPrior[["lambda"]]), + "spike" = list("location" = optionsPrior[["x0"]]) + ) + + if(!arguments[["distribution"]] %in% c("spike", "uniform")) { + arguments[["truncation"]] <- list( + lower = optionsPrior[["truncationLower"]], + upper = optionsPrior[["truncationUpper"]] + ) + } + + return(do.call(BayesTools::prior, arguments)) +} + +.bpcsMuPriorFromOptions <- function(options) { + if (options$priorSettings == "default") { + return("Jeffreys_mu") + } else { + comp <- .bpcsPriorComponentByName(options, "mean") + return(.bpcsPriorFromComponent(comp, "mu")) + } +} + +.bpcsSigmaPriorFromOptions <- function(options) { + if (options$priorSettings == "default") { + return("Jeffreys_sigma") + } else { + comp <- .bpcsPriorComponentByName(options, "sigma") + return(.bpcsPriorFromComponent(comp, "sigma")) + } +} +.bpcsTPriorFromOptions <- function(options) { + + switch(options[["capabilityStudyType"]], + "normalCapabilityAnalysis" = NULL, + "tCapabilityAnalysis" = .bpcsPriorFromComponent(.bpcsPriorComponentByName(options, "df"), "df"), + + stop("Unknown capability study type: ", options[["capabilityStudyType"]]) + ) +} + +.bpcsPriorHelper <- function(options) { + if (options$priorSettings == "default") { + if (options[["capabilityStudyType"]] == "normalCapabilityAnalysis") { + return("DCSI") + } + return("Jeffreys") + } + + mu_prior <- .bpcsMuPriorFromOptions(options) + sigma_prior <- .bpcsSigmaPriorFromOptions(options) + nu_prior <- .bpcsTPriorFromOptions(options) + + args <- list(mu = mu_prior, sigma = sigma_prior) + if (!is.null(nu_prior)) { + args$nu <- nu_prior + } + + do.call(qc::prior_independent, args) +} + +# Tables ---- +.bpcsCapabilityTable <- function(jaspResults, dataset, options, position) { + + # Check if we already have the results cached + if (!is.null(jaspResults[["bpcsCapabilityTable"]])) + return(.bpcsSamplePosteriorOrPrior(jaspResults, dataset, options)) # will return object from state (if it exists) + + table <- .bpcsCapabilityTableMeta(jaspResults, options, position = position) + if (!.bpcsIsReady(options)) { + + if (options[["measurementLongFormat"]] != "" || length(options[["measurementsWideFormat"]]) > 0) + table$addFootnote(gettext( + "Please specify the Lower Specification Limit, Upper Specification Limit, and Target Value to compute the capability measures." + )) + + return(NULL) + } + + resultsObject <- .bpcsSamplePosteriorOrPrior(jaspResults, dataset, options) + + .bpcsCapabilityTableFill(table, resultsObject, options) + return(resultsObject) + +} + +.bpcsSamplePosteriorOrPrior <- function(jaspResults, dataset, options, prior = FALSE) { + + base <- if (prior) "bpcsPriors" else "bpcs" + if (prior && !.bpcsCanSampleFromPriors(options)) + return(NULL) + + if (!is.null(jaspResults[[paste0(base, "ResultsObject")]])) + return(jaspResults[[paste0(base, "ResultsObject")]]$object) + + x <- if (ncol(dataset) > 0L) dataset[[1L]] else NULL + + rawfit <- jaspResults[[paste0(base, "State")]] %setOrRetrieve% ( + qc::bpc( + x, chains = 1, warmup = 1000, iter = 5000, silent = TRUE, seed = 1, + target = options[["targetValue"]], + LSL = options[["lowerSpecificationLimitValue"]], + USL = options[["upperSpecificationLimitValue"]], + prior = .bpcsPriorHelper(options), + sample_priors = prior + ) |> + createJaspState(jaspDeps(.bpcsStateDeps())) + ) + + summaryObject <- jaspResults[[paste0(base, "SummaryState")]] %setOrRetrieve% ( + summary( + rawfit, ci.level = options[["credibleIntervalWidth"]] + ) |> + createJaspState(jaspDeps( + options = c(.bpcsStateDeps(), "credibleIntervalWidth") + )) + ) + + resultsObject <- list( + rawfit = rawfit, + summaryObject = summaryObject + ) + + jaspResults[[paste0(base, "ResultsObject")]] <- createJaspState(resultsObject) + + return(resultsObject) +} + +.bpcsCanSampleFromPriors <- function(options) { + if (options$priorSettings != "default") { + return(TRUE) + } + options[["capabilityStudyType"]] == "normalCapabilityAnalysis" +} + +.bpcsCapabilityTableMeta <- function(jaspResults, options, position) { + + table <- createJaspTable(title = gettext("Capability Table"), position = position) + table$addColumnInfo(name = "metric", title = gettext("Measure"), type = "string") + table$addColumnInfo(name = "mean", title = gettext("Mean"), type = "number") + table$addColumnInfo(name = "median", title = gettext("Median"), type = "number") + table$addColumnInfo(name = "sd", title = gettext("Std"), type = "number") + + overtitle <- gettextf("%s%% Credible Interval", 100 * options[["credibleIntervalWidth"]]) + table$addColumnInfo(name = "lower", title = gettext("Lower"), type = "number", overtitle = overtitle) + table$addColumnInfo(name = "upper", title = gettext("Upper"), type = "number", overtitle = overtitle) + + table$dependOn(c(.bpcsDefaultDeps(), "credibleIntervalWidth")) + + jaspResults[["bpcsCapabilityTable"]] <- table + return(table) + +} + +.bpcsGetSelectedMetrics <- function(options) { + allMetrics <- c("Cp", "CpU", "CpL", "Cpk", "Cpc", "Cpm") + selectedMetrics <- allMetrics[c(options[["Cp"]], options[["Cpu"]], options[["Cpl"]], + options[["Cpk"]], options[["Cpc"]], options[["Cpm"]])] + return(selectedMetrics) +} + +getCustomAxisLimits <- function(options, base) { + keys <- c(paste0(base, "custom_x_", c("min", "max")), paste0(base, "custom_y_", c("min", "max"))) + values <- lapply(keys, function(k) options[[k]]) + names(values) <- c("xmin", "xmax", "ymin", "ymax") + values +} +# end utils + +.bpcsCapabilityTableFill <- function(table, resultsObject, options) { + + df <- as.data.frame(resultsObject[["summaryObject"]][["summary"]]) + + # Filter metrics based on user selection + selectedMetrics <- .bpcsGetSelectedMetrics(options) + + if (length(selectedMetrics) > 0) { + df <- df[df$metric %in% selectedMetrics, , drop = FALSE] + } + + table$setData(df) + +} + +.bpcsIntervalTable <- function(jaspResults, options, fit, position) { + + if (!options[["intervalTable"]]) + return() + + table <- .bpcsIntervalTableMeta(jaspResults, options, position) + if (!.bpcsIsReady(options) || is.null(fit)) + return() + + selectedMetrics <- .bpcsGetSelectedMetrics(options) + tryCatch({ + + # qc does c(-Inf, interval_probability, Inf) + interval_probability <- unlist(options[paste0("interval", 1:4)], use.names = FALSE) + interval_summary <- summary(fit[["rawfit"]], interval_probability = interval_probability)[["interval_summary"]] + colnames(interval_summary) <- c("metric", paste0("interval", 1:5)) + interval_summary <- subset(interval_summary, metric %in% selectedMetrics) + table$setData(interval_summary) + + }, error = function(e) { + + table$setError(gettextf("Unexpected error in interval table: %s", e$message)) + + }) + + return() +} + +.bpcsIntervalTableMeta <- function(jaspResults, options, position) { + + table <- createJaspTable(title = gettext("Interval Table"), position = position) + + table$addColumnInfo(name = "metric", title = gettext("Capability\nMeasure"), type = "string") + + intervalBounds <- c(-Inf, unlist(options[paste0("interval", 1:4)], use.names = FALSE), Inf) + intervalNames <- unlist(options[paste0("intervalLabel", 1:5)], use.names = FALSE) + n <- length(intervalBounds) + + # custom format helper. we don't use e.g., %.3f directly because that adds trailing zeros (2.000 instead of 2) + fmt <- \(x) formatC(x, digits = 3, format = "f", drop0trailing = TRUE) + for (i in 1:(n - 1)) { + j <- i + 1 + lhs <- if (i == 1) "(" else "[" + rhs <- if (i == n - 1) ")" else "]" + title <- sprintf("%s %s%s, %s%s", intervalNames[i], lhs, fmt(intervalBounds[i]), fmt(intervalBounds[j]), rhs) + table$addColumnInfo(name = paste0("interval", i), title = title, type = "number") + } + table$dependOn(c("intervalTable", .bpcsDefaultDeps(), .bpcsProcessCriteriaDeps())) + + jaspResults[["bpcsIntervalTable"]] <- table + return(table) +} + + +# Plots ---- +.bpcsCapabilityPlot <- function(jaspResults, options, fit, priorFit, position, base = "posteriorDistributionPlot") { + + if (!options[[base]] || !is.null(jaspResults[[base]])) + return() + + singlePanel <- options[[paste0(base, "PanelLayout")]] != "multiplePanels" + + isPost <- base == "posteriorDistributionPlot" + summaryObject <- if (isPost) fit$summaryObject else priorFit$summaryObject + # only if the user asked for it + priorSummaryObject <- if (isPost && options[[paste0(base, "PriorDistribution")]]) priorFit$summaryObject else NULL + + jaspPlt <- createJaspPlot( + title = if (isPost) gettext("Posterior Distribution") else gettext("Prior Distribution"), + width = 400 * (if (singlePanel) 1 else 3), + height = 400 * (if (singlePanel) 1 else 2), + position = position, + dependencies = jaspDeps( + options = c( + .bpcsDefaultDeps(), + # .bpcsPosteriorPlotDeps(options), + .bpcsPlotLayoutDeps(base, hasType = FALSE) + ) + ) + ) + jaspResults[[base]] <- jaspPlt + + if (!.bpcsIsReady(options) || (isPost && is.null(fit)) || (!isPost && is.null(priorFit))) + return() + + if (!isPost && !.bpcsCanSampleFromPriors(options)) { + jaspPlt$width <- 400 + jaspPlt$height <- 400 + jaspPlt$setError(gettext("Prior distribution cannot be shown for improper priors.")) + return() + } + + tryCatch({ + + # Get selected metrics + selectedMetrics <- .bpcsGetSelectedMetrics(options) + + if (length(selectedMetrics) == 0) { + NULL + } else { + + jaspPlt$plotObject <- qc::plot_density( + summaryObject, + what = selectedMetrics, + point_estimate = if (options[[paste0(base, "IndividualPointEstimate")]]) options[[paste0(base, "IndividualPointEstimateType")]] else "none", + ci = if (options[[paste0(base, "IndividualCi")]]) options[[paste0(base, "IndividualCiType")]] else "none", + ci_level = options[[paste0(base, "IndividualCiMass")]], + ci_custom_left = options[[paste0(base, "IndividualCiLower")]], + ci_custom_right = options[[paste0(base, "IndividualCiUpper")]], + bf_support = options[[paste0(base, "IndividualCiBf")]], + single_panel = singlePanel, + axes = options[[paste0(base, "Axes")]], + axes_custom = getCustomAxisLimits(options, base), + priorSummaryObject = priorSummaryObject + ) + + jaspGraphs::geom_rangeframe() + + jaspGraphs::themeJaspRaw() + + + } + }, error = function(e) { + jaspPlt$width <- 400 + jaspPlt$height <- 400 + jaspPlt$setError( + if (isPost) gettextf("Unexpected error in posterior distribution plot: %s", e$message) + else gettextf("Unexpected error in prior distribution plot: %s", e$message) + ) + }) + +} + +# .bpcsPosteriorPlotDeps <- function(options) { +# c( +# "posteriorDistributionPlot", +# "posteriorDistributionPlotIndividualPointEstimate", +# "posteriorDistributionPlotIndividualPointEstimateType", +# "posteriorDistributionPlotPriorDistribution", +# "posteriorDistributionPlotIndividualCi", +# "posteriorDistributionPlotIndividualCiType", +# # these match which options are conditionally enabled in the qml file. +# switch(options[["posteriorDistributionPlotIndividualCiType"]], +# "central" = "posteriorDistributionPlotIndividualCiMass", +# "HPD" = "posteriorDistributionPlotIndividualCiMass", +# "custom" = c("posteriorDistributionPlotIndividualCiLower", "posteriorDistributionPlotIndividualCiUpper"), +# "support" = "posteriorDistributionPlotIndividualCiBf" +# ) +# ) +# } + +.bpcsSequentialPointEstimatePlot <- function(jaspResults, dataset, options, fit, position) { + + base <- "sequentialAnalysisPointEstimatePlot" + # "sequentialAnalysisPointIntervalPlot" + if (!options[[base]] || !is.null(jaspResults[[base]])) + return() + + w <- 400 + plt <- createJaspPlot(title = gettext("Sequential Analysis Point Estimate"), width = 3*w, height = 2*w, + position = position, + dependencies = jaspDeps(c( + .bpcsDefaultDeps(), + .bpcsPlotLayoutDeps(base, hasPrior = FALSE), + "sequentialAnalysisPlotAdditionalInfo" + ))) + jaspResults[[base]] <- plt + + if (!.bpcsIsReady(options) || jaspResults$getError()) return() + + sequentialPlotData <- .bpcsGetSequentialAnalysis(jaspResults, dataset, options, fit) + + if (!is.null(sequentialPlotData$error)) { + plt$setError(sequentialPlotData$error) + } else { + tryCatch({ + plt$plotObject <- .bpcsMakeSequentialPlot(sequentialPlotData$data, options, base) + }, error = function(e) { + plt$setError(gettextf("Unexpected error in sequential analysis point estimate plot: %s", e$message)) + } + ) + } +} + +.bpcsSequentialIntervalEstimatePlot <- function(jaspResults, dataset, options, fit, position) { + + # base <- "sequentialAnalysisPointEstimatePlot" + base <- "sequentialAnalysisPointIntervalPlot" + if (!options[[base]] || !is.null(jaspResults[[base]])) + return() + + w <- 400 + plt <- createJaspPlot(title = gettext("Sequential Analysis Interval Estimate"), width = 3*w, height = 2*w, + position = position, + dependencies = jaspDeps(c( + .bpcsDefaultDeps(), + .bpcsPlotLayoutDeps(base, hasPrior = FALSE) + ))) + jaspResults[[base]] <- plt + + if (!.bpcsIsReady(options) || jaspResults$getError()) return() + + sequentialPlotData <- .bpcsGetSequentialAnalysis(jaspResults, dataset, options, fit) + + if (!is.null(sequentialPlotData$error)) { + plt$setError(sequentialPlotData$error) + } else { + tryCatch({ + plt$plotObject <- .bpcsMakeSequentialPlot(sequentialPlotData$data, options, base, custom = TRUE) + }, error = function(e) { + plt$setError(gettextf("Unexpected error in sequential analysis interval estimate plot: %s", e$message)) + } + ) + } +} + +.bpcsGetSequentialAnalysis <- function(jaspResults, dataset, options, fit) { + + if (!.bpcsIsReady(options) || jaspResults$getError()) return() + + base1 <- "sequentialAnalysisPointEstimatePlot" + base2 <- "sequentialAnalysisPointIntervalPlot" + + baseData <- "SequentialAnalysisData" + tryCatch({ + sequentialPlotData <- jaspResults[[baseData]] %setOrRetrieve% ( + .bpcsComputeSequentialAnalysis(dataset, options, fit) |> + createJaspState(dependencies = jaspDeps( + options = c(.bpcsStateDeps(), + paste0(base2, c("TypeLower", "TypeUpper"))) + )) + ) + + return(list(data = sequentialPlotData, error = NULL)) + + }, error = function(e) { + + return(list(data = NULL, error = e$message)) + + }) + +} + +.bpcsComputeSequentialAnalysis <- function(dataset, options, fit) { + + n <- nrow(dataset) + if (!is.finite(n) || n < 3L) { + stop("Sequential analysis requires at least 3 observations.", call. = FALSE) + } + nfrom <- 3L + nto <- n + nby <- 1L + nseq <- seq(nfrom, nto, by = nby) + estimates <- array(NA, c(6, 5, length(nseq))) + + hasCustom <- options$sequentialAnalysisPointIntervalPlot + customBounds <- c(options$sequentialAnalysisPointIntervalPlotTypeLower, + options$sequentialAnalysisPointIntervalPlotTypeUpper) + + keys <- c("mean", "median", "lower", "upper", "custom") + dimnames(estimates) <- list(list(), keys, list()) + + x <- dataset[[1L]] + + jaspBase::startProgressbar(length(nseq), label = gettext("Running sequential analysis")) + + prior <- .bpcsPriorHelper(options) + n_failed <- 0L + for (i in seq_along(nseq)) { + + x_i <- x[1:nseq[i]] + fit_i <- tryCatch( + qc::bpc( + x_i, chains = 1, warmup = 1000, iter = 5000, silent = TRUE, seed = 1, + target = options[["targetValue"]], + LSL = options[["lowerSpecificationLimitValue"]], + USL = options[["upperSpecificationLimitValue"]], + prior = prior + ), + error = function(e) NULL + ) + + if (is.null(fit_i)) { + n_failed <- n_failed + 1L + jaspBase::progressbarTick() + next + } + + sum_fit_i <- summary(fit_i, interval_probability = customBounds) + sum_i <- sum_fit_i$summary + custom_i <- sum_fit_i$interval_summary[, 3, drop = FALSE] + colnames(custom_i) <- "custom" + sum_i <- cbind(sum_i, custom_i) + + if (is.null(rownames(estimates))) + rownames(estimates) <- sum_i$metric + + estimates[, , i] <- as.matrix(sum_i[keys]) + jaspBase::progressbarTick() + } + + if (n_failed > 0L && n_failed / length(nseq) > 0.1) { + stop( + sprintf( + "%d of %d sequential fits failed (%.0f%%). Cannot render plot.", + n_failed, length(nseq), 100 * n_failed / length(nseq) + ), + call. = FALSE + ) + } + + attr(estimates, "nseq") <- nseq + + # we could use this one, but only if the CI width is exactly equal to the one requested here. + # that would be nice to add at some point so the values in the table are identical to those in the plot + # sum_n <- summary(fit)$summary + # estimates[, , n] <- as.matrix(sum_n[keys]) + + return(estimates) +} + +.bpcsMakeSequentialPlot <- function(estimates, options, base, custom = FALSE) { + + # this function should move to qc, and these are the arguments that should be passed to the arguments of that function + single_panel <- options[[paste0(base, "PanelLayout")]] != "multiplePanels" + axes <- options[[paste0(base, "Axes")]] + axes_custom <- getCustomAxisLimits(options, base) + + pointEstimateOption <- paste0(base, "IndividualPointEstimateType") + pointEstimateName <- if (options[[pointEstimateOption]] == "mean") "mean" else "median" + add_additional_info <- options[["sequentialAnalysisPlotAdditionalInfo"]] + + selectedMetrics <- .bpcsGetSelectedMetrics(options) + if (length(selectedMetrics) == 0L) + return(NULL) + + ciOption <- paste0(base, "IndividualCi") + has_ci <- options[[ciOption]] + + if (custom) { + has_ci <- FALSE + pointEstimateName <- "custom" + add_additional_info <- FALSE + y_limits <- c(0, 1) + y_title <- gettextf("P(%1$.3f \u2264 x \u2264 %2$.3f)", + options$sequentialAnalysisPointIntervalPlotTypeLower, + options$sequentialAnalysisPointIntervalPlotTypeUpper) + } else { + + y_title <- if (has_ci) { + gettextf("Estimate with 95%% credible interval") + } else { + gettext("Estimate") + } + } + + # this is somewhat ugly, but we convert the 3d array to a tibble for plotting + # we don't create the tibble immediately in the previous function, because + # it takes up more space in the state (which means larger jasp files) + + categoryNames <- c(gettext("Incapable"), gettext("Capable"), gettext("Satisfactory"), gettext("Excellent"), gettext("Super")) + gridLines <- c(1, 4/3, 3/2, 2) + # the extrema are missing here, these should be determined based on any leftover space. + defaultCategoryPositions <- (gridLines[-1] + gridLines[-length(gridLines)]) / 2 + + nseq <- attr(estimates, "nseq") + + tb <- tibble::tibble( + metric = factor(rep(rownames(estimates), times = length(nseq))), + n = rep(nseq, each = nrow(estimates)), + mean = as.vector(estimates[, pointEstimateName, ]), + lower = as.vector(estimates[, "lower", ]), + upper = as.vector(estimates[, "upper", ]), + ) + tb <- tb[tb$metric %in% selectedMetrics, , drop = FALSE] + if (length(selectedMetrics) == 1L) + single_panel <- TRUE + + # get y scales per facet + if (single_panel) { + + observedRange <- range(tb$lower, tb$upper, na.rm = TRUE) + if (!all(is.finite(observedRange))) { + observedRange <- c(0, 1) + } + dist <- observedRange[2L] - observedRange[1L] + + observedRange[1L] <- min(observedRange[1L], gridLines[1L] - 0.1 * dist) + observedRange[2L] <- max(observedRange[2L], gridLines[length(gridLines)] + 0.1 * dist) + + leftBreaks <- jaspGraphs::getPrettyAxisBreaks(observedRange) + leftLimits <- range(leftBreaks) + + rightAxis <- ggplot2::waiver() + if (add_additional_info) { + rightBreaksShown <- c( + (leftLimits[1L] + gridLines[1L]) / 2, + defaultCategoryPositions, + (leftLimits[2L] + gridLines[length(gridLines)]) / 2 + ) + rightBreaks <- numeric(2L*length(rightBreaksShown) + 1L) + rightBreaks[1L] <- leftLimits[1L] + rightBreaks[seq(2, length(rightBreaks), 2)] <- rightBreaksShown + rightBreaks[seq(3, length(rightBreaks) - 2, 2)] <- gridLines + rightBreaks[length(rightBreaks)] <- leftLimits[2L] + + rightLabels <- character(length(rightBreaks)) + rightLabels[seq(2, length(rightLabels), 2)] <- categoryNames + rightAxis <- ggplot2::sec_axis(identity, breaks = rightBreaks, labels = rightLabels) + } + + y_breaks_per_scale <- ggplot2::scale_y_continuous(breaks = leftBreaks, limits = range(leftBreaks), + minor_breaks = gridLines, + sec.axis = rightAxis) + + } else { + y_breaks_per_scale <- tapply(tb, tb$metric, \(x) { + + # x <- tb[tb$metric == tb$metric[1L], , drop = FALSE] + observedRange <- range(x$lower, x$upper, na.rm = TRUE) + if (!all(is.finite(observedRange))) { + observedRange <- c(0, 1) + } + dist <- observedRange[2L] - observedRange[1L] + + observedRange[1L] <- min(observedRange[1L], gridLines[1L] - 0.1 * dist) + observedRange[2L] <- max(observedRange[2L], gridLines[length(gridLines)] + 0.1 * dist) + + if (custom) { + observedRange[1L] <- max(observedRange[1L], y_limits[1L]) + observedRange[2L] <- min(observedRange[2L], y_limits[2L]) + } + + leftBreaks <- jaspGraphs::getPrettyAxisBreaks(observedRange) + leftLimits <- range(leftBreaks) + + rightAxis <- ggplot2::waiver() + if (add_additional_info) { + rightBreaksShown <- c( + (leftLimits[1L] + gridLines[1L]) / 2, + defaultCategoryPositions, + (leftLimits[2L] + gridLines[length(gridLines)]) / 2 + ) + rightBreaks <- numeric(2L*length(rightBreaksShown) + 1L) + rightBreaks[1L] <- leftLimits[1L] + rightBreaks[seq(2, length(rightBreaks), 2)] <- rightBreaksShown + rightBreaks[seq(3, length(rightBreaks) - 2, 2)] <- gridLines + rightBreaks[length(rightBreaks)] <- leftLimits[2L] + + rightLabels <- character(length(rightBreaks)) + rightLabels[seq(2, length(rightLabels), 2)] <- categoryNames + rightAxis <- ggplot2::sec_axis(identity, breaks = rightBreaks, labels = rightLabels) + } + + ggplot2::scale_y_continuous(breaks = leftBreaks, limits = range(leftBreaks), + minor_breaks = gridLines, + sec.axis = rightAxis) + }, simplify = FALSE) + } + + ribbon <- NULL + if (has_ci) + ribbon <- ggplot2::geom_ribbon(ggplot2::aes(ymin = .data$lower, ymax = .data$upper), alpha = 0.3) + + extraTheme <- gridLinesLayer <- NULL + sides <- "bl" + if (add_additional_info) { + # there are 11 ticks, the outermost we hide (NA) because one of their bounds is infinite + # the inner ticks alternate between black and NA, so there is a tick at the grid lines + # but no tick at the criteria text (which is secretly an axis tick label). + rightTickColors <- c(NA, rep(c(NA, "black"), length.out = 9), NA) + extraTheme <- ggplot2::theme(axis.ticks.y.right = ggplot2::element_line(colour = rightTickColors)) + sides <- "blr" + # I tried using minor.breaks for this, but these are not drawn properly with facet_grid and facetted_pos_scales + gridLinesLayer <- ggplot2::geom_hline( + data = data.frame(yintercept = gridLines), + ggplot2::aes(yintercept = .data$yintercept), + # show.legend = FALSE, + linewidth = .5, color = "lightgray", linetype = "dashed" + ) + + } + + scale_x <- scale_facet <- facet <- NULL + noMetrics <- nrow(estimates) + if (noMetrics == 1L || single_panel) { + xBreaks <- jaspGraphs::getPrettyAxisBreaks(tb$n) + xLimits <- range(tb$n) + scale_x <- ggplot2::scale_x_continuous(breaks = xBreaks, limits = xLimits) + scale_facet <- y_breaks_per_scale + } else { + scales <- switch(axes, + "automatic" = "free_y", + "fixed" = "fixed", + "free" = "free_y", + "custom" = "fixed", + stop("Unknown axes option.") + ) + if (axes == "custom") { + if (!is.null(axes_custom[["xmin"]]) && !is.null(axes_custom[["xmax"]])) { + xbreaks <- jaspGraphs::getPrettyAxisBreaks(c(axes_custom[["xmin"]], axes_custom[["xmax"]])) + scale_x <- ggplot2::scale_x_continuous(limits = sort(c(axes_custom[["xmin"]], axes_custom[["xmax"]]))) + } + if (!is.null(axes_custom[["ymin"]]) && !is.null(axes_custom[["ymax"]])) { + ybreaks <- jaspGraphs::getPrettyAxisBreaks(c(axes_custom[["ymin"]], axes_custom[["ymax"]])) + leftLimits <- sort(c(axes_custom[["ymin"]], axes_custom[["ymax"]])) + rightAxis <- ggplot2::waiver() + if (add_additional_info) { + rightBreaksShown <- c( + (leftLimits[1L] + gridLines[1L]) / 2, + defaultCategoryPositions, + (leftLimits[2L] + gridLines[length(gridLines)]) / 2 + ) + rightBreaks <- numeric(2L*length(rightBreaksShown) + 1L) + rightBreaks[1L] <- leftLimits[1L] + rightBreaks[seq(2, length(rightBreaks), 2)] <- rightBreaksShown + rightBreaks[seq(3, length(rightBreaks) - 2, 2)] <- gridLines + rightBreaks[length(rightBreaks)] <- leftLimits[2L] + + rightLabels <- character(length(rightBreaks)) + rightLabels[seq(2, length(rightLabels), 2)] <- categoryNames + rightAxis <- ggplot2::sec_axis(identity, breaks = rightBreaks, labels = rightLabels) + } + scale_facet <- ggplot2::scale_y_continuous(breaks = ybreaks, limits = leftLimits, + minor_breaks = gridLines, sec.axis = rightAxis) + } + } else if (axes == "automatic" || axes == "free") { + scale_facet <- ggh4x::facetted_pos_scales(y = y_breaks_per_scale) + } + facet <- ggplot2::facet_wrap(~metric, scales = scales) + } + + ggplot2::ggplot(tb, ggplot2::aes(x = .data$n, y = .data$mean, group = .data$metric, + color = .data$metric, fill = .data$metric)) + + gridLinesLayer + + ribbon + + ggplot2::geom_line(linewidth = 1) + + facet + scale_facet + scale_x + + ggplot2::labs( + x = gettext("Number of observations"), + y = y_title, + color = gettext("Metric"), + fill = gettext("Metric") + ) + + jaspGraphs::geom_rangeframe(sides = sides) + + jaspGraphs::themeJaspRaw(legend.position = if (single_panel) "right" else "none") + + extraTheme + +} + +# Additional plot functions ---- +.bpcsPlotPredictive <- function(jaspResults, dataset, options, fit, position, base = c("posteriorPredictiveDistributionPlot", "priorPredictiveDistributionPlot")) { + + base <- match.arg(base) + isPrior <- base == "priorPredictiveDistributionPlot" + + if (!options[[base]] || !is.null(jaspResults[[base]])) + return() + + plot <- createJaspPlot( + title = if (isPrior) gettext("Prior predictive distribution") else gettext("Posterior Predictive Distribution"), + width = 400, + height = 400, + position = position, + dependencies = c( + .bpcsDefaultDeps(), + base, + paste0(base, "IndividualPointEstimate"), + paste0(base, "IndividualPointEstimateType"), + paste0(base, "IndividualCi"), + paste0(base, "IndividualCiType"), + paste0(base, "IndividualCiMass"), + paste0(base, "IndividualCiLower"), + paste0(base, "IndividualCiUpper") + )) + + jaspResults[[base]] <- plot + + if (!.bpcsIsReady(options) || is.null(fit) || jaspResults$getError()) return() + + tryCatch({ + rawfit <- fit$rawfit + if (identical(rawfit$method, "integration")) { + if (inherits(rawfit$prior_resolved, "PriorConjugate")) { + # based on Murphy, K. P. (2007). Conjugate Bayesian analysis of the Gaussian distribution. def, 1(2σ2), 16. + # TODO: since we have access to the distribution we could avoid sampling and plot the density directly + prior <- rawfit$prior_resolved + state <- rawfit$integration_result$cached_state + post <- qc:::.nig_posterior(prior, state$n, state$x_bar, state$sse) + df <- 2 * post$alpha_n + scale <- sqrt(post$beta_n * (1 + 1 / post$k_n) / post$alpha_n) + predictiveSamples <- post$mu_n + scale * stats::rt(5000, df) + } else { + rawfit <- qc::bpc( + x = if (ncol(dataset) > 0L) dataset[[1L]] else NULL, + method = "mcmc", + distribution = rawfit$distribution %||% "normal", + prior = rawfit$prior, + LSL = options[["lowerSpecificationLimitValue"]], + USL = options[["upperSpecificationLimitValue"]], + target = options[["targetValue"]], + chains = 1, warmup = 1000, iter = 5000, silent = TRUE, seed = 1, + sample_priors = isPrior + ) + raw_samples <- qc:::extract_samples(rawfit, bootstrap = FALSE) + samples <- qc:::samples_to_mu_and_sigma(raw_samples) + predictiveSamples <- qc:::samples_to_posterior_predictives(samples) + } + } else { + raw_samples <- qc:::extract_samples(rawfit, bootstrap = FALSE) + samples <- qc:::samples_to_mu_and_sigma(raw_samples) + predictiveSamples <- qc:::samples_to_posterior_predictives(samples) + } + + plt <- jaspGraphs::jaspHistogram( + predictiveSamples, + xName = if (isPrior) gettext("Prior predictive") else gettext("Posterior predictive"), + density = TRUE + ) + + # Calculate density for positioning elements above histogram + dens <- stats::density(predictiveSamples) + maxDensity <- max(dens$y) + + # Add point estimate if requested + if (options[[paste0(base, "IndividualPointEstimate")]]) { + pointEstimateType <- options[[paste0(base, "IndividualPointEstimateType")]] + pointEstimate <- switch(pointEstimateType, + "mean" = mean(predictiveSamples), + "median" = stats::median(predictiveSamples), + "mode" = dens$x[which.max(dens$y)] + ) + plt <- plt + ggplot2::geom_point( + data = data.frame(x = pointEstimate, y = 0), + ggplot2::aes(x = .data$x, y = .data$y), + size = 3, + inherit.aes = FALSE + ) + } + + # Add CI if requested + if (options[[paste0(base, "IndividualCi")]]) { + ciType <- options[[paste0(base, "IndividualCiType")]] + + ciInterval <- if (ciType == "custom") { + c(options[[paste0(base, "IndividualCiLower")]], + options[[paste0(base, "IndividualCiUpper")]]) + } else { + ciMass <- options[[paste0(base, "IndividualCiMass")]] / 100 + if (ciType == "central") { + stats::quantile(predictiveSamples, probs = c((1 - ciMass) / 2, (1 + ciMass) / 2)) + } else if (ciType == "HPD") { + # For HPD, we need HDInterval package or implement it + if (requireNamespace("HDInterval", quietly = TRUE)) { + HDInterval::hdi(predictiveSamples, credMass = ciMass) + } else { + # Fallback to central interval + stats::quantile(predictiveSamples, probs = c((1 - ciMass) / 2, (1 + ciMass) / 2)) + } + } + } + + # Position errorbar above the histogram + yPosition <- maxDensity * 1.1 + plt <- plt + ggplot2::geom_errorbarh( + data = data.frame(x = mean(ciInterval), xmin = ciInterval[1], xmax = ciInterval[2], y = yPosition), + ggplot2::aes(x = .data$x, xmin = .data$xmin, xmax = .data$xmax, y = .data$y), + height = maxDensity * 0.05, + linewidth = 0.75, + inherit.aes = FALSE + ) + } + + plot$plotObject <- plt + }, error = function(e) { + plot$setError( + if (isPrior) gettextf("Unexpected error in prior predictive distribution plot: %s", e$message) + else gettextf("Unexpected error in posterior predictive distribution plot: %s", e$message) + ) + }) +} diff --git a/inst/qml/bayesianProcessCapabilityStudies.qml b/inst/qml/bayesianProcessCapabilityStudies.qml new file mode 100644 index 0000000..8bfa4f9 --- /dev/null +++ b/inst/qml/bayesianProcessCapabilityStudies.qml @@ -0,0 +1,463 @@ +// Copyright (C) 2013-2018 University of Amsterdam +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// You should have received a copy of the GNU Affero General Public +// License along with this program. If not, see +// . +// + +import QtQuick +import QtQuick.Layouts +import JASP.Controls + +import "./common" as Common + +Form +{ + function sortIntervalValues() { + + var values = [ + interval1.displayValue, + interval2.displayValue, + interval3.displayValue, + interval4.displayValue + ] + values.sort(function(a, b) { return a - b }) + interval1.value = values[0] + interval2.value = values[1] + interval3.value = values[2] + interval4.value = values[3] + interval1b.value = values[0] + interval2b.value = values[1] + interval3b.value = values[2] + interval4b.value = values[3] + } + function sortIntervalValuesb() { + + var values = [ + interval1b.displayValue, + interval2b.displayValue, + interval3b.displayValue, + interval4b.displayValue + ] + values.sort(function(a, b) { return a - b }) + interval1.value = values[0] + interval2.value = values[1] + interval3.value = values[2] + interval4.value = values[3] + interval1b.value = values[0] + interval2b.value = values[1] + interval3b.value = values[2] + interval4b.value = values[3] + } + columns: 2 + + VariablesForm + { + id: variablesFormLongFormat + + AvailableVariablesList + { + name: "variablesFormLongFormat" + } + + AssignedVariablesList + { + name: "measurementLongFormat" + title: qsTr("Measurement") + id: measurementLongFormat + allowedColumns: ["scale"] + singleVariable: true + } + + } + + + // Section + // { + // title: qsTr("Process capability options") + + Group + { + title: qsTr("Type of data distribution") + + + RadioButtonGroup + { + name: "capabilityStudyType" + id: capabilityStudyType + + RadioButton + { + name: "normalCapabilityAnalysis" + id : normalCapabilityAnalysis + label: qsTr("Normal distribution") + checked: true + } + + RadioButton + { + name: "tCapabilityAnalysis" + id : tCapabilityAnalysis + label: qsTr("Student's t-distribution") + // checked: true + } + + } + } + + Group + { + columns: 2 + title: qsTr("Metrics") + info: qsTr("Select the process capability metrics to report.") + CheckBox { name: "Cp"; label: qsTr("Cp"); checked: true } + CheckBox { name: "Cpu"; label: qsTr("Cpu"); checked: true } + CheckBox { name: "Cpl"; label: qsTr("Cpl"); checked: true } + CheckBox { name: "Cpk"; label: qsTr("Cpk"); checked: true } + CheckBox { name: "Cpc"; label: qsTr("Cpc"); checked: true } + CheckBox { name: "Cpm"; label: qsTr("Cpm"); checked: true } + } + + Group + { + title: qsTr("Capability Study") + + CheckBox + { + name: "lowerSpecificationLimit" + label: qsTr("Lower specification limit") + id: lowerSpecificationLimit + childrenOnSameRow: true + + DoubleField + { + name: "lowerSpecificationLimitValue" + id: lowerSpecificationLimitValue + negativeValues: true + defaultValue: -1 + decimals: 9 + } + + } + + CheckBox + { + name: "target" + label: qsTr("Target value") + id: target + childrenOnSameRow: true + + DoubleField + { + name: "targetValue" + id: targetValue + negativeValues: true + defaultValue: 0 + decimals: 9 + } + } + + CheckBox + { + name: "upperSpecificationLimit" + label: qsTr("Upper specification limit") + id: upperSpecificationLimit + childrenOnSameRow: true + + DoubleField + { + name: "upperSpecificationLimitValue" + id: upperSpecificationLimitValue + negativeValues: true + defaultValue: 1 + decimals: 9 + } + + } + + } + + Group + { + + title: qsTr("Process Criteria") + GridLayout + { + // title: qsTr("Process Criteria") + columns: 5 + columnSpacing: 2 + rowSpacing: jaspTheme.rowGridSpacing / 3 + id: intervalRow + property int dbWidth: 50 + property int txtWidth: 100 + + // Row 0: Headers + Label {text: qsTr("Left bound")} + Item{} + Label {text: qsTr("Classification")} + Item{} + Label {text: qsTr("Right bound")} + + // Row 1: Incapable + Item{} + Item{} + TextField { name: "intervalLabel1"; defaultValue: qsTr("Incapable"); fieldWidth: intervalRow.txtWidth} + Label { text: "<"; } + DoubleField { name: "interval1"; id: interval1; fieldWidth: intervalRow.dbWidth; defaultValue: 1.00; onEditingFinished: sortIntervalValues() } + + // Row 2: Capable + DoubleField { name: "interval1b";id: interval1b; fieldWidth: intervalRow.dbWidth; editable: true; value: interval1.value; onEditingFinished: {sortIntervalValuesb()} } + Label { text: "<"; } + TextField { name: "intervalLabel2"; defaultValue: qsTr("Capable"); fieldWidth: intervalRow.txtWidth} + Label { text: "≤"; } + DoubleField { name: "interval2"; id: interval2; fieldWidth: intervalRow.dbWidth; defaultValue: 1.33; onEditingFinished: sortIntervalValues() } + + // Row 3: Satisfactory + DoubleField { name: "interval2b"; id: interval2b; fieldWidth: intervalRow.dbWidth; editable: true; value: interval2.value; onEditingFinished: {sortIntervalValuesb()} } + Label { text: "<"; } + TextField { name: "intervalLabel3"; defaultValue: qsTr("Satisfactory"); fieldWidth: intervalRow.txtWidth} + Label { text: "≤"; } + DoubleField { name: "interval3"; id: interval3; fieldWidth: intervalRow.dbWidth; defaultValue: 1.50; onEditingFinished: sortIntervalValues() } + + // Row 4: Excellent + DoubleField { name: "interval3b"; id: interval3b; fieldWidth: intervalRow.dbWidth; editable: true; value: interval3.value; onEditingFinished: {sortIntervalValuesb()} } + Label { text: "<"; } + TextField { name: "intervalLabel4"; defaultValue: qsTr("Excellent"); fieldWidth: intervalRow.txtWidth} + Label { text: "≤"; } + DoubleField { name: "interval4"; id: interval4; fieldWidth: intervalRow.dbWidth; defaultValue: 2.00; onEditingFinished: sortIntervalValues() } + + // Row 5: Super + DoubleField { name: "interval4b"; id: interval4b; fieldWidth: intervalRow.dbWidth; editable: true; value: interval4.value; onEditingFinished: {sortIntervalValuesb()} } + Label { text: ">"; } + TextField { name: "intervalLabel5"; defaultValue: qsTr("Super"); fieldWidth: intervalRow.txtWidth} + Item{} + Item{} + } + } + + // } + + // Section + // { + // title: qsTr("Prior distributions") + + // } + + Section + { + title: qsTr("Tables") + CheckBox + { + name: "intervalTable" + label: qsTr("Interval table") + info: qsTr("Show the posterior probabilities of the interval specified with the input on the right. Note that the input is automatically sorted and that the first and last fields are always negative and positive infinity.") + } + CIField + { + name: "credibleIntervalWidth" + label: qsTr("Credible interval") + info: qsTr("Width of the credible interval used for the posterior distribution in the Capability table.") + } + } + + Section + { + + title: qsTr("Prior and Posterior Inference") + + Common.PlotLayout {} + + Common.PlotLayout + { + baseName: "priorDistributionPlot" + baseLabel: qsTr("Prior distribution") + hasPrior: false + } + + } + + Section + { + title: qsTr("Sequential Analysis") + + Common.PlotLayout + { + id: sequentialAnalysisPointEstimatePlot + baseName: "sequentialAnalysisPointEstimatePlot" + baseLabel: qsTr("Point estimate plot") + hasPrior: false + } + + Common.PlotLayout + { + id: sequentialAnalysisIntervalEstimatePlot + baseName: "sequentialAnalysisPointIntervalPlot" + baseLabel: qsTr("Interval estimate plot") + hasPrior: false + hasEstimate: false + hasCi: false + hasType: true + } + + Group + { + CheckBox + { + enabled: sequentialAnalysisPointEstimatePlot.checked || sequentialAnalysisIntervalEstimatePlot.checked + id: sequentialAnalysisAdditionalInfo + name: "sequentialAnalysisPlotAdditionalInfo" + label: qsTr("Show process criteria") + checked: true + info: qsTr("Add a secondary right axis with condition bounds for the process") + } + + CheckBox + { + // TODO: + enabled: sequentialAnalysisPointEstimatePlot.checked || sequentialAnalysisIntervalEstimatePlot.checked + name: "sequentialAnalysisUpdatingTable" + label: qsTr("Posterior updating table") + checked: false + info: qsTr("Show the data from the sequential analysis in a table. Will show both the information for the point estimate and interval estimate plots, if both are selected.") + } + } + } + + Section + { + + title: qsTr("Prior and Posterior Predictive Plots") + + Common.PlotLayout + { + baseName: "posteriorPredictiveDistributionPlot" + baseLabel: qsTr("Posterior predictive distribution") + hasPrior: false + hasAxes: false + hasPanels: false + } + + Common.PlotLayout + { + baseName: "priorPredictiveDistributionPlot" + baseLabel: qsTr("Prior predictive distribution") + hasPrior: false + hasAxes: false + hasPanels: false + } + + } + + + Section + { + title: qsTr("Prior distributions") + + // TODO: this dropdown should just show the same GUI as the custom one + // but disable e.g., the DropDown itself and instead show the prior + // also disable all truncation for non-custom ones + // NOTE: the above is done, but default values cannot be set yet. + + DropDown + { + id: priorSettings + name: "priorSettings" + label: qsTr("Prior distributions") + values: + [ + {label: qsTr("Default"), value: "default"}, + {label: qsTr("Informed conjugate"), value: "conjugate"}, + // {label: qsTr("Informed conjugate"), value: "weaklyInformativeConjugate"}, + {label: qsTr("Informed uniform"), value: "weaklyInformativeUniform"}, + {label: qsTr("Custom informative"), value: "customInformative"}, + ] + } + + Common.Priors + { + + // visible: priorSettings.currentValue === "customInformative" + priorType: capabilityStudyType.value === "normalCapabilityAnalysis" ? "normalModel" : "tModel" + + hasTruncation: priorSettings.currentValue === "customInformative" + hasParameters: priorSettings.currentValue !== "default" + visible: priorSettings.currentValue !== "default" + + dropDownValuesMap: { + switch (priorSettings.currentValue) { + case "default": + return { + "mean": [{ label: qsTr("Jeffreys"), value: "jeffreys"}], + "sigma": [{ label: qsTr("Jeffreys"), value: "jeffreys"}], + "df": [{ label: qsTr("Gamma(α,β)"), value: "gammaAB" }] + } + case "conjugate": + return { + "mean": [{ label: qsTr("Normal(μ,σ)"), value: "normal"}], + "sigma": [{ label: qsTr("Gamma(α,β)"), value: "gammaAB" }], + "df": [{ label: qsTr("Gamma(α,β)"), value: "gammaAB" }] + }; + // case "weaklyInformativeConjugate": + // return { + // "mean": [{ label: qsTr("Normal(μ,σ)"), value: "normal"}], + // "sigma": [{ label: qsTr("Gamma(α,β)"), value: "gammaAB" }], + // "df": [{ label: qsTr("Gamma(α,β)"), value: "gammaAB" }] + // } + case "weaklyInformativeUniform": + return { + "mean": [{ label: qsTr("Uniform(a,b)"), value: "uniform"}], + "sigma": [{ label: qsTr("Uniform(a,b)"), value: "uniform"}], + "df": [{ label: qsTr("Gamma(α,β)"), value: "gammaAB" }] + } + case "customInformative": + return undefined; + } + } + } + } + + Section + { + title: qsTr("Advanced options") + + Group + { + title: qsTr("MCMC Settings") + info: qsTr("Adjust the Markov Chain Monte Carlo (MCMC) settings for estimating the posterior distribution.") + IntegerField + { + name: "noIterations" + label: qsTr("No. iterations") + defaultValue: 5000 + min: 100 + max: 100000000 + info: qsTr("Number of MCMC iterations used for estimating the posterior distribution.") + } + IntegerField + { + name: "noWarmup" + label: qsTr("No. warmup samples") + defaultValue: 1000 + min: 0 + max: 100000000 + info: qsTr("Number of initial MCMC samples to discard.") + } + IntegerField + { + name: "noChains" + label: qsTr("No. chains") + defaultValue: 1 + min: 1 + max: 128 + info: qsTr("Number of MCMC chains to run.") + } + } + } +} diff --git a/inst/qml/common/PlotLayout.qml b/inst/qml/common/PlotLayout.qml new file mode 100644 index 0000000..abc3957 --- /dev/null +++ b/inst/qml/common/PlotLayout.qml @@ -0,0 +1,266 @@ +// +// Copyright (C) 2013-2018 University of Amsterdam +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public +// License along with this program. If not, see +// . +// +import QtQuick +import QtQuick.Layouts +import JASP +import JASP.Controls + + +Group +{ + id: root + property string baseName: "posteriorDistributionPlot" + property string baseLabel: qsTr("Posterior distribution") + property bool hasPrior: true + property bool hasEstimate: true + property bool hasCi: true + property bool hasType: false + property bool hasAxes: true + property bool hasPanels: true + + readonly property alias checked: mainCheckBox.checked + + CheckBox + { + id: mainCheckBox + name: baseName + label: baseLabel + + // Group so the options are shown in a 2-column layout + Group + { + + columns: 2 + columnSpacing: 10 * jaspTheme.columnGroupSpacing + + // Group so point estimate and CI options are shown in a single column + Group + { + enabled: hasEstimate || hasCi + visible: hasEstimate || hasCi + + CheckBox + { + enabled: hasEstimate + visible: hasEstimate + label: qsTr("Point estimate") + name: baseName + "IndividualPointEstimate" + childrenOnSameRow: true + + DropDown + { + name: baseName + "IndividualPointEstimateType" + label: "" + values: [ + {label: qsTr("mean"), value: "mean"}, + {label: qsTr("median"), value: "median"}, + {label: qsTr("mode"), value: "mode"} + ] + } + } + + // Group so CI checkbox and options are shown in a single column (with subgroup so CI options are indented) + Group + { + enabled: hasCi + visible: hasCi + + columns: 1 + CheckBox + { + name: baseName + "IndividualCi" + label: qsTr("CI") + id: posteriorPlotIndividualCI + childrenOnSameRow: true + + DropDown + { + name: baseName + "IndividualCiType" + label: "" + id: posteriorPlotIndividualType + values: [ + {label: qsTr("central"), value: "central"}, + {label: qsTr("HPD"), value: "HPD"}, + {label: qsTr("custom"), value: "custom"}//, + // {label: qsTr("support"), value: "support"} + ] + } + } + + Group + { + columns: 2 + indent: true + enabled: posteriorPlotIndividualCI.checked + + CIField + { + visible: posteriorPlotIndividualType.currentValue === "central" || posteriorPlotIndividualType.currentValue === "HPD" + name: baseName + "IndividualCiMass" + label: qsTr("Mass") + fieldWidth: 50 + defaultValue: 95 + min: 1 + max: 100 + inclusive: JASP.MinMax + } + + DoubleField + { + visible: posteriorPlotIndividualType.currentValue === "custom" + name: baseName + "IndividualCiLower" + label: qsTr("Lower") + id: plotsPosteriorLower + fieldWidth: 50 + defaultValue: 0 + negativeValues: true + inclusive: JASP.MinMax + } + + DoubleField + { + visible: posteriorPlotIndividualType.currentValue === "custom" + name: baseName + "IndividualCiUpper" + label: qsTr("Upper") + id: plotsPosteriorUpper + fieldWidth: 50 + defaultValue: 1 + negativeValues: true + inclusive: JASP.MinMax + } + + FormulaField + { + visible: posteriorPlotIndividualType.currentValue === "support" + name: baseName + "IndividualCiBf" + label: qsTr("BF") + fieldWidth: 50 + defaultValue: "1" + min: 0 + inclusive: JASP.None + } + } + } + } + + Group + { + enabled: hasType + visible: hasType + + title: qsTr("Type") + + columns: 2 + FormulaField + { + name: baseName + "TypeLower" + label: qsTr("Lower") + id: typeLower + fieldWidth: 50 + defaultValue: 0.0 + max: typeUpper.value + } + + FormulaField + { + name: baseName + "TypeUpper" + label: qsTr("Upper") + id: typeUpper + fieldWidth: 50 + defaultValue: 1.0 + min: typeLower.value + + } + } + + RadioButtonGroup + { + enabled: hasPanels + visible: hasPanels + name: baseName + "PanelLayout" + title: qsTr("Layout") + id: posteriorDistributionPlotPanelLayout + + RadioButton { value: "multiplePanels"; label: qsTr("One plot per metric"); checked: true } + RadioButton { value: "singlePanel"; label: qsTr("All metrics in one plot") } + + } + + RadioButtonGroup + { + enabled: hasAxes + visible: hasAxes + name: baseName + "Axes" + title: qsTr("Axes") + id: posteriorDistributionPlotAxes + + RadioButton { value: "free"; label: qsTr("Automatic"); checked: true } + RadioButton { value: "fixed"; label: qsTr("Identical across panels"); enabled: posteriorDistributionPlotPanelLayout.value === "multiplePanels" } + RadioButton { value: "custom"; label: qsTr("Custom axes"); } + } + + Group + { + + title: qsTr("Custom axes") + enabled: hasAxes && posteriorDistributionPlotAxes.value === "custom" + visible: hasAxes && posteriorDistributionPlotAxes.value === "custom" + + GridLayout + { + columns: 5 + columnSpacing: 2 + rowSpacing: jaspTheme.rowGridSpacing / 3 + id: customAxesLayout + property int dbWidth: 50 + property int txtWidth: 100 + + // Row 0: Headers + Label {text: qsTr("Axis")} + Item{} + Label {text: qsTr("Min")} + Item{} + Label {text: qsTr("Max")} + + // Row 1: x axis + Label { text: qsTr("x axis"); } + Item{} + DoubleField { name: baseName + "custom_x_min"; id: custom_x_min; fieldWidth: customAxesLayout.dbWidth; defaultValue: 0.00; negativeValues: true; max: custom_x_max.value} + Item{} + DoubleField { name: baseName + "custom_x_max"; id: custom_x_max; fieldWidth: customAxesLayout.dbWidth; defaultValue: 1.00; negativeValues: true; min: custom_x_min.value} + + // Row 2: y axis + Label { text: qsTr("y axis"); } + Item{} + DoubleField { name: baseName + "custom_y_min"; id: custom_y_min; fieldWidth: customAxesLayout.dbWidth; defaultValue: 0.00; negativeValues: false; max: custom_y_max.value} + Item{} + DoubleField { name: baseName + "custom_y_max"; id: custom_y_max; fieldWidth: customAxesLayout.dbWidth; defaultValue: 1.00; negativeValues: false; min: custom_y_min.value} + } + } + + CheckBox + { + enabled: hasPrior + visible: hasPrior + name: baseName + "PriorDistribution" + label: qsTr("Show prior distribution") + checked: false + } + } + } +} \ No newline at end of file diff --git a/inst/qml/common/Priors.qml b/inst/qml/common/Priors.qml new file mode 100644 index 0000000..1693e73 --- /dev/null +++ b/inst/qml/common/Priors.qml @@ -0,0 +1,352 @@ +// +// Copyright (C) 2013-2018 University of Amsterdam +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public +// License along with this program. If not, see +// . +// +import QtQuick +import QtQuick.Layouts +import JASP.Controls +import JASP + +ColumnLayout +{ + spacing: 0 + property string priorType: "normalModel" + property bool hasTruncation: false + property bool hasParameters: true + + Component.onCompleted: { + console.log("Component completed, priorType: " + priorType); + console.log("Current component values: " + JSON.stringify(currentComponentValues)); + } + + onPriorTypeChanged: { + // this is not shown? + console.log("Prior type changed to: " + priorType); + } + + // TODO: these should not be fixed, no? + // property var meanValues: { "name": "mean", "type": "normal", "mu": "0", "sigma": "1" } + // property var sigmaValues: { "name": "sigma", "type": "invgamma", "alpha": "1", "beta": "0.15", "truncationLower": 0 } + // property var dfValues: { "name": "t", "type": "invgamma", "alpha": "1", "beta": "0.15", "truncationLower": 0, "hasJeffreys": false } + + property var nameMap: { + "mean": "Mean", + "sigma": "Sigma", + "df": "df" + } + property var defaultDistributionMap: { + "mean": "normal", + "sigma": "invgamma", + "df": "invgamma" + } + property var truncationLowerMap: { + "mean": -Infinity, + "sigma": 0, + "df": 0 + } + property var allPriors : [ + { label: qsTr("Normal(μ,σ)"), value: "normal"}, + { label: qsTr("Student-t(μ,σ,v)"), value: "t"}, + { label: qsTr("Cauchy(x₀,θ)"), value: "cauchy"}, + { label: qsTr("Jeffreys"), value: "jeffreys"}, + { label: qsTr("Gamma(α,β)"), value: "gammaAB"}, + { label: qsTr("Gamma(k,θ)"), value: "gammaK0"}, + { label: qsTr("Inverse-Gamma(α,β)"), value: "invgamma"}, + { label: qsTr("Log-Normal(μ,σ)"), value: "lognormal"}, + { label: qsTr("Beta(α,β)"), value: "beta"}, + { label: qsTr("Uniform(a,b)"), value: "uniform"} + ] + property var priorTruncationMap: { + "normal" : [-Infinity, Infinity], + "t" : [-Infinity, Infinity], + "cauchy" : [-Infinity, Infinity], + "jeffreys": [-Infinity, Infinity], + "gammaAB": [0, Infinity], + "gammaK0": [0, Infinity], + "invgamma": [0, Infinity], + "lognormal": [0, Infinity], + "beta": [0, 1 ], + "uniform": [-Infinity, Infinity] + } + property var defaultDropDownValuesMap: { + "mean": allPriors, + "sigma": allPriors, + "df": allPriors.filter(p => p.value !== "jeffreys") + } + property var dropDownValuesMap: undefined + property var activeDropDownValuesMap: dropDownValuesMap !== undefined ? dropDownValuesMap : defaultDropDownValuesMap + property var hasJeffreysMap: { + "mean": true, + "sigma": true, + "df": false + } + + onDropDownValuesMapChanged: console.log("dropDownValuesMap changed: " + dropDownValuesMap) + // property var defaultParametersMap: { + // "mean": { "mu": "0", "sigma": "1" }, + // "sigma": { "alpha": "1", "beta": "0.15", "truncationLower": 0 }, + // "t": { "alpha": "1", "beta": "0.15", "truncationLower": 0, "hasJeffreys": false } + // } + + property var currentComponentValues: { + switch (priorType) { + case "normalModel": + return [ "mean", "sigma" ]; + case "tModel": + return [ "mean", "sigma", "df" ]; + } + // switch (priorType) { + // case "normalModel": + // return [ meanValues, sigmaValues ]; + // case "tModel": + // return [ meanValues, sigmaValues, dfValues ]; + // } + } + + + // TODO: this could also be a gridLayout, no? + property double width1: 70 * preferencesModel.uiScale; + property double width2: 140 * preferencesModel.uiScale; + property double width3: 155 * preferencesModel.uiScale; + property double width4: 130 * preferencesModel.uiScale; + + RowLayout + { + Label { text: qsTr("Parameter"); Layout.preferredWidth: width1; Layout.leftMargin: 5 * preferencesModel.uiScale} + Label { text: qsTr("Distribution"); Layout.preferredWidth: width2; Layout.leftMargin: 5 * preferencesModel.uiScale} + Label { text: qsTr("Parameters"); Layout.preferredWidth: width3 ; visible: hasParameters } + Label { text: qsTr("Truncation"); Layout.preferredWidth: width4 ; visible: hasTruncation } + } + + + ComponentsList + { + name: priorType + "ComponentsList" + optionKey: "name" + + addItemManually: false + + // defaultValues: currentComponentValues + values: currentComponentValues + + rowComponent: RowLayout + { + Row + { + spacing: 4 * preferencesModel.uiScale + Layout.preferredWidth: width1 + Label { text: nameMap[rowValue] } + } + + Row + { + spacing: 4 * preferencesModel.uiScale + Layout.preferredWidth: width2 + + DropDown + { + visible: activeDropDownValuesMap[rowValue].length > 1 + id: typeItem + name: "type" + useExternalBorder: true + value: defaultDistributionMap[rowValue] + values: activeDropDownValuesMap[rowValue] + } + + Label + { + visible: activeDropDownValuesMap[rowValue].length === 1 + text: activeDropDownValuesMap[rowValue][0].label + } + } + + Row + { + spacing: 4 * preferencesModel.uiScale + Layout.preferredWidth: width3 + visible: hasParameters + + FormulaField + { + label: "μ" + name: "mu" + visible: typeItem.currentValue === "normal" || + typeItem.currentValue === "lognormal" || + typeItem.currentValue === "t" + value: "0" + inclusive: JASP.None + fieldWidth: 40 * preferencesModel.uiScale + useExternalBorder: false + showBorder: true + + } + FormulaField + { + label: "x₀" + name: "x0" + visible: typeItem.currentValue === "cauchy" || + typeItem.currentValue === "spike" + value: "0" + inclusive: JASP.None + fieldWidth: 40 * preferencesModel.uiScale + useExternalBorder: false + showBorder: true + } + FormulaField + { + label: "σ" + name: "sigma" + id: sigma + visible: typeItem.currentValue === "normal" || + typeItem.currentValue === "lognormal" || + typeItem.currentValue === "t" + value: "1" + min: 0 + inclusive: JASP.None + fieldWidth: 40 * preferencesModel.uiScale + useExternalBorder: false + showBorder: true + } + FormulaField + { + label: "k " + name: "k" + visible: typeItem.currentValue === "gammaK0" + value: "1" + min: 0 + inclusive: JASP.None + fieldWidth: 40 * preferencesModel.uiScale + useExternalBorder: false + } + FormulaField + { + label: "θ" + name: "theta" + visible: typeItem.currentValue === "cauchy" || + typeItem.currentValue === "gammaK0" + value: "1" + min: 0 + inclusive: JASP.None + fieldWidth: 40 * preferencesModel.uiScale + useExternalBorder: false + showBorder: true + } + FormulaField + { + label: "ν" + name: "nu" + visible: typeItem.currentValue === "t" + value: "2" + min: 1 + inclusive: JASP.MinOnly + fieldWidth: 40 * preferencesModel.uiScale + useExternalBorder: false + showBorder: true + } + FormulaField + { + label: "α " + name: "alpha" + visible: typeItem.currentValue === "gammaAB" || + typeItem.currentValue === "invgamma" || + typeItem.currentValue === "beta" + value: "1" + min: 0 + inclusive: JASP.None + fieldWidth: 40 * preferencesModel.uiScale + useExternalBorder: false + showBorder: true + } + FormulaField + { + label: "β" + name: "beta" + visible: typeItem.currentValue === "gammaAB" || + typeItem.currentValue === "invgamma" || + typeItem.currentValue === "beta" + value: "0.15" + min: 0 + inclusive: JASP.None + fieldWidth: 40 * preferencesModel.uiScale + useExternalBorder: false + showBorder: true + } + FormulaField + { + label: "a " + name: "a" + id: a + visible: typeItem.currentValue === "uniform" + value: "0" + max: b.value + inclusive: JASP.None + fieldWidth: 40 * preferencesModel.uiScale + useExternalBorder: false + showBorder: true + } + FormulaField + { + label: "b" + name: "b" + id: b + visible: typeItem.currentValue === "uniform" + value: "1" + min: a.value + inclusive: JASP.None + fieldWidth: 40 * preferencesModel.uiScale + useExternalBorder: false + showBorder: true + } + } + + Row + { + spacing: 4 * preferencesModel.uiScale + Layout.preferredWidth: width4 + + FormulaField + { + id: truncationLower + label: qsTr("lower") + name: "truncationLower" + visible: hasTruncation && typeItem.currentValue !== "spike" && typeItem.currentValue !== "uniform" && typeItem.currentValue !== "jeffreys" + value: Math.max((priorTruncationMap[typeItem.currentValue] || [-Infinity, Infinity])[0], truncationLowerMap[rowValue]) + min: Math.max((priorTruncationMap[typeItem.currentValue] || [-Infinity, Infinity])[0], truncationLowerMap[rowValue]) + max: truncationUpper.value + inclusive: JASP.MinOnly + fieldWidth: 40 * preferencesModel.uiScale + useExternalBorder: false + showBorder: true + } + FormulaField + { + id: truncationUpper + label: qsTr("upper") + name: "truncationUpper" + visible: hasTruncation && typeItem.currentValue !== "spike" && typeItem.currentValue !== "uniform" && typeItem.currentValue !== "jeffreys" + value: (priorTruncationMap[typeItem.currentValue] || [-Infinity, Infinity])[1] + max: (priorTruncationMap[typeItem.currentValue] || [-Infinity, Infinity])[1] + min: truncationLower ? truncationLower.value : 0 + inclusive: JASP.MaxOnly + fieldWidth: 40 * preferencesModel.uiScale + useExternalBorder: false + showBorder: true + } + } + } + } + +} From 898f6de61d1905288dfeff3619a05d94459bd4c7 Mon Sep 17 00:00:00 2001 From: Julius Pfadt Date: Thu, 6 Aug 2026 09:48:11 +0200 Subject: [PATCH 03/14] fix: adapt to current qc API, wire up MCMC settings, correct plot deps - qc:::samples_to_mu_and_sigma no longer exists; replace the three qc::: internals with exported qc::extract_predictive_samples, which handles the integration method natively (incl. degenerate data) and drops the hand-rolled NIG predictive - noChains/noWarmup/noIterations were dependencies but never read, so the MCMC settings in the GUI had no effect; pass them to all qc::bpc calls - interval estimate plot declared hasEstimate/hasCi deps and omitted hasType, the inverse of its Common.PlotLayout flags, so the Lower/Upper fields did not invalidate the plot (reported by julianwuth on #414) - rename getCustomAxisLimits -> .bpcsGetCustomAxisLimits, it leaked into the package namespace - drop unused rlang import Co-Authored-By: Claude Opus 5 (1M context) --- R/bayesianProcessCapabilityStudies.R | 51 ++++++++++++---------------- 1 file changed, 22 insertions(+), 29 deletions(-) diff --git a/R/bayesianProcessCapabilityStudies.R b/R/bayesianProcessCapabilityStudies.R index f344e81..05a71c0 100644 --- a/R/bayesianProcessCapabilityStudies.R +++ b/R/bayesianProcessCapabilityStudies.R @@ -15,8 +15,7 @@ # along with this program. If not, see . # -#'@importFrom jaspBase jaspDeps %setOrRetrieve% -#'@importFrom rlang .data +#'@importFrom jaspBase jaspDeps %setOrRetrieve% createJaspPlot createJaspState createJaspTable #'@export @@ -235,7 +234,8 @@ bayesianProcessCapabilityStudies <- function(jaspResults, dataset, options) { rawfit <- jaspResults[[paste0(base, "State")]] %setOrRetrieve% ( qc::bpc( - x, chains = 1, warmup = 1000, iter = 5000, silent = TRUE, seed = 1, + x, chains = options[["noChains"]], warmup = options[["noWarmup"]], iter = options[["noIterations"]], + silent = TRUE, seed = 1, target = options[["targetValue"]], LSL = options[["lowerSpecificationLimitValue"]], USL = options[["upperSpecificationLimitValue"]], @@ -297,7 +297,7 @@ bayesianProcessCapabilityStudies <- function(jaspResults, dataset, options) { return(selectedMetrics) } -getCustomAxisLimits <- function(options, base) { +.bpcsGetCustomAxisLimits <- function(options, base) { keys <- c(paste0(base, "custom_x_", c("min", "max")), paste0(base, "custom_y_", c("min", "max"))) values <- lapply(keys, function(k) options[[k]]) names(values) <- c("xmin", "xmax", "ymin", "ymax") @@ -432,7 +432,7 @@ getCustomAxisLimits <- function(options, base) { bf_support = options[[paste0(base, "IndividualCiBf")]], single_panel = singlePanel, axes = options[[paste0(base, "Axes")]], - axes_custom = getCustomAxisLimits(options, base), + axes_custom = .bpcsGetCustomAxisLimits(options, base), priorSummaryObject = priorSummaryObject ) + jaspGraphs::geom_rangeframe() + @@ -514,7 +514,8 @@ getCustomAxisLimits <- function(options, base) { position = position, dependencies = jaspDeps(c( .bpcsDefaultDeps(), - .bpcsPlotLayoutDeps(base, hasPrior = FALSE) + # mirrors the flags set on this plot's Common.PlotLayout in the qml + .bpcsPlotLayoutDeps(base, hasPrior = FALSE, hasEstimate = FALSE, hasCi = FALSE, hasType = TRUE) ))) jaspResults[[base]] <- plt @@ -591,7 +592,8 @@ getCustomAxisLimits <- function(options, base) { x_i <- x[1:nseq[i]] fit_i <- tryCatch( qc::bpc( - x_i, chains = 1, warmup = 1000, iter = 5000, silent = TRUE, seed = 1, + x_i, chains = options[["noChains"]], warmup = options[["noWarmup"]], iter = options[["noIterations"]], + silent = TRUE, seed = 1, target = options[["targetValue"]], LSL = options[["lowerSpecificationLimitValue"]], USL = options[["upperSpecificationLimitValue"]], @@ -644,7 +646,7 @@ getCustomAxisLimits <- function(options, base) { # this function should move to qc, and these are the arguments that should be passed to the arguments of that function single_panel <- options[[paste0(base, "PanelLayout")]] != "multiplePanels" axes <- options[[paste0(base, "Axes")]] - axes_custom <- getCustomAxisLimits(options, base) + axes_custom <- .bpcsGetCustomAxisLimits(options, base) pointEstimateOption <- paste0(base, "IndividualPointEstimateType") pointEstimateName <- if (options[[pointEstimateOption]] == "mean") "mean" else "median" @@ -900,18 +902,12 @@ getCustomAxisLimits <- function(options, base) { tryCatch({ rawfit <- fit$rawfit - if (identical(rawfit$method, "integration")) { - if (inherits(rawfit$prior_resolved, "PriorConjugate")) { - # based on Murphy, K. P. (2007). Conjugate Bayesian analysis of the Gaussian distribution. def, 1(2σ2), 16. - # TODO: since we have access to the distribution we could avoid sampling and plot the density directly - prior <- rawfit$prior_resolved - state <- rawfit$integration_result$cached_state - post <- qc:::.nig_posterior(prior, state$n, state$x_bar, state$sse) - df <- 2 * post$alpha_n - scale <- sqrt(post$beta_n * (1 + 1 / post$k_n) / post$alpha_n) - predictiveSamples <- post$mu_n + scale * stats::rt(5000, df) - } else { - rawfit <- qc::bpc( + predictiveSamples <- tryCatch( + qc::extract_predictive_samples(rawfit), + # qc can only draw predictives from an integration fit when the prior is conjugate; + # for any other prior refit with mcmc so the plot can still be shown + error = function(e) { + mcmcfit <- qc::bpc( x = if (ncol(dataset) > 0L) dataset[[1L]] else NULL, method = "mcmc", distribution = rawfit$distribution %||% "normal", @@ -919,18 +915,15 @@ getCustomAxisLimits <- function(options, base) { LSL = options[["lowerSpecificationLimitValue"]], USL = options[["upperSpecificationLimitValue"]], target = options[["targetValue"]], - chains = 1, warmup = 1000, iter = 5000, silent = TRUE, seed = 1, + chains = options[["noChains"]], + warmup = options[["noWarmup"]], + iter = options[["noIterations"]], + silent = TRUE, seed = 1, sample_priors = isPrior ) - raw_samples <- qc:::extract_samples(rawfit, bootstrap = FALSE) - samples <- qc:::samples_to_mu_and_sigma(raw_samples) - predictiveSamples <- qc:::samples_to_posterior_predictives(samples) + qc::extract_predictive_samples(mcmcfit) } - } else { - raw_samples <- qc:::extract_samples(rawfit, bootstrap = FALSE) - samples <- qc:::samples_to_mu_and_sigma(raw_samples) - predictiveSamples <- qc:::samples_to_posterior_predictives(samples) - } + ) plt <- jaspGraphs::jaspHistogram( predictiveSamples, From f2a402e16eecf8ff3cfc49db0facd029b752bd61 Mon Sep 17 00:00:00 2001 From: Julius Pfadt Date: Thu, 6 Aug 2026 11:58:45 +0200 Subject: [PATCH 04/14] fix: use qc metric name casing so Cpu and Cpl are not silently dropped qc returns metrics named Cpu/Cpl and errors on CpU/CpL, but .bpcsGetSelectedMetrics built CpU/CpL. the %in% filters in the capability table, interval table and sequential plot therefore dropped both metrics, and qc::plot_density(what=) would have rejected them, so ticking Cpu or Cpl in the GUI produced nothing. Co-Authored-By: Claude Opus 5 (1M context) --- R/bayesianProcessCapabilityStudies.R | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/R/bayesianProcessCapabilityStudies.R b/R/bayesianProcessCapabilityStudies.R index 05a71c0..3ae175b 100644 --- a/R/bayesianProcessCapabilityStudies.R +++ b/R/bayesianProcessCapabilityStudies.R @@ -291,7 +291,8 @@ bayesianProcessCapabilityStudies <- function(jaspResults, dataset, options) { } .bpcsGetSelectedMetrics <- function(options) { - allMetrics <- c("Cp", "CpU", "CpL", "Cpk", "Cpc", "Cpm") + # casing must match the metric names qc uses, it errors on CpU / CpL + allMetrics <- c("Cp", "Cpu", "Cpl", "Cpk", "Cpc", "Cpm") selectedMetrics <- allMetrics[c(options[["Cp"]], options[["Cpu"]], options[["Cpl"]], options[["Cpk"]], options[["Cpc"]], options[["Cpm"]])] return(selectedMetrics) From a46c83cae5a9e1df1665f3e87dcd500df53efb02 Mon Sep 17 00:00:00 2001 From: Julius Pfadt Date: Thu, 6 Aug 2026 12:01:57 +0200 Subject: [PATCH 05/14] test: add coverage for bayesian process capability study #414 shipped with no tests. covers the capability table, metric selection, interval table, distribution and predictive plots, readiness, and both regressions fixed here (metric name casing, plot dependencies). includes a helper supplying the options jaspTools cannot read, it does not expand the Common.PlotLayout / Common.Priors qml components. Co-Authored-By: Claude Opus 5 (1M context) --- tests/testthat/datasets/processCapability.csv | 41 +++++ .../test-bayesianProcessCapabilityStudies.R | 165 ++++++++++++++++++ 2 files changed, 206 insertions(+) create mode 100644 tests/testthat/datasets/processCapability.csv create mode 100644 tests/testthat/test-bayesianProcessCapabilityStudies.R diff --git a/tests/testthat/datasets/processCapability.csv b/tests/testthat/datasets/processCapability.csv new file mode 100644 index 0000000..f85452e --- /dev/null +++ b/tests/testthat/datasets/processCapability.csv @@ -0,0 +1,41 @@ +"measurement" +10.685 +9.718 +10.182 +10.316 +10.202 +9.947 +10.756 +9.953 +11.009 +9.969 +10.652 +11.143 +9.306 +9.861 +9.933 +10.318 +9.858 +8.672 +8.78 +10.66 +9.847 +9.109 +9.914 +10.607 +10.948 +9.785 +9.871 +9.118 +10.23 +9.68 +10.228 +10.352 +10.518 +9.696 +10.252 +9.141 +9.608 +9.575 +8.793 +10.018 diff --git a/tests/testthat/test-bayesianProcessCapabilityStudies.R b/tests/testthat/test-bayesianProcessCapabilityStudies.R new file mode 100644 index 0000000..ff24562 --- /dev/null +++ b/tests/testthat/test-bayesianProcessCapabilityStudies.R @@ -0,0 +1,165 @@ +context("[Bayesian Quality Control] Bayesian Process Capability Study") + +# jaspTools cannot expand the Common.PlotLayout / Common.Priors components used by +# inst/qml/bayesianProcessCapabilityStudies.qml, so analysisOptions() returns an +# incomplete set. These helpers supply the missing options with their qml defaults. +.plotLayoutOptions <- function(base, checked = FALSE) { + o <- list() + o[[base]] <- checked + o[[paste0(base, "IndividualPointEstimate")]] <- FALSE + o[[paste0(base, "IndividualPointEstimateType")]] <- "mean" + o[[paste0(base, "IndividualCi")]] <- FALSE + o[[paste0(base, "IndividualCiType")]] <- "central" + o[[paste0(base, "IndividualCiMass")]] <- 95 + o[[paste0(base, "IndividualCiLower")]] <- 0 + o[[paste0(base, "IndividualCiUpper")]] <- 1 + o[[paste0(base, "IndividualCiBf")]] <- "1" + o[[paste0(base, "TypeLower")]] <- 0 + o[[paste0(base, "TypeUpper")]] <- 1 + o[[paste0(base, "PanelLayout")]] <- "multiplePanels" + o[[paste0(base, "Axes")]] <- "free" + o[[paste0(base, "custom_x_min")]] <- 0 + o[[paste0(base, "custom_x_max")]] <- 1 + o[[paste0(base, "custom_y_min")]] <- 0 + o[[paste0(base, "custom_y_max")]] <- 1 + o[[paste0(base, "PriorDistribution")]] <- FALSE + o +} + +.plotBases <- c("posteriorDistributionPlot", "priorDistributionPlot", + "sequentialAnalysisPointEstimatePlot", "sequentialAnalysisPointIntervalPlot", + "posteriorPredictiveDistributionPlot", "priorPredictiveDistributionPlot") + +# Default options for the analysis, with spec limits matched to datasets/processCapability.csv +# (40 observations, roughly normal around 10 with sd 0.5). +.bpcsOptions <- function() { + options <- analysisOptions("bayesianProcessCapabilityStudies") + extra <- c(do.call(c, lapply(.plotBases, .plotLayoutOptions)), + list(axisLabels = FALSE, normalModelComponentsList = list(), tModelComponentsList = list())) + options[names(extra)] <- extra + + options$capabilityStudyType <- "normalCapabilityAnalysis" + options$measurementLongFormat <- "measurement" + options$priorSettings <- "default" + options$lowerSpecificationLimit <- TRUE + options$lowerSpecificationLimitValue <- 8.5 + options$target <- TRUE + options$targetValue <- 10 + options$upperSpecificationLimit <- TRUE + options$upperSpecificationLimitValue <- 11.5 + # keep the sampler cheap, this analysis refits per observation in the sequential plots + options$noChains <- 1 + options$noWarmup <- 200 + options$noIterations <- 1000 + options +} + +.capabilityRows <- function(results) { + rows <- results[["results"]][["bpcsCapabilityTable"]][["data"]] + do.call(rbind, lapply(rows, function(r) as.data.frame(r, stringsAsFactors = FALSE))) +} + +## Capability table #### + +options <- .bpcsOptions() +set.seed(1) +results <- runAnalysis("bayesianProcessCapabilityStudies", "datasets/processCapability.csv", options) + +test_that("Analysis runs to completion", { + expect_equal(results[["status"]], "complete") + expect_null(results[["results"]][["errorMessage"]]) +}) + +test_that("Capability table reports every metric the user selected", { + # regression: qc names these Cpu/Cpl and errors on CpU/CpL, mismatched casing + # silently dropped both metrics from the table + expect_equal(.capabilityRows(results)$metric, c("Cp", "Cpu", "Cpl", "Cpk", "Cpc", "Cpm")) +}) + +test_that("Capability table estimates are plausible for a well centred process", { + df <- .capabilityRows(results) + # LSL 8.5, USL 11.5, sd about 0.5 => Cp near 1 + expect_equal(df$mean[df$metric == "Cp"], 1.0, tolerance = 0.25) + # the sampler makes these stochastic, so only assert the ordering that must hold + expect_true(all(df$lower < df$mean)) + expect_true(all(df$mean < df$upper)) + expect_true(df$mean[df$metric == "Cpk"] <= df$mean[df$metric == "Cp"]) +}) + +test_that("Deselecting metrics removes them from the table", { + options <- .bpcsOptions() + options$Cpu <- FALSE + options$Cpl <- FALSE + options$Cpc <- FALSE + options$Cpm <- FALSE + set.seed(1) + results <- runAnalysis("bayesianProcessCapabilityStudies", "datasets/processCapability.csv", options) + expect_equal(.capabilityRows(results)$metric, c("Cp", "Cpk")) +}) + +## Estimation #### + +test_that("Estimates are deterministic across runs", { + # qc::bpc defaults to method = "integration", so the fit is numerical rather + # than sampled and repeated runs must agree exactly. + # + # NOTE: this is also why the MCMC Settings group in the qml currently has no + # effect on the capability table. noChains/noWarmup/noIterations are passed to + # qc::bpc (they used to be ignored entirely) but only take effect on the mcmc + # path, which the analysis never selects because there is no qml control for + # the estimation method. Either add that control or drop the settings group. + options <- .bpcsOptions() + set.seed(1) + first <- runAnalysis("bayesianProcessCapabilityStudies", "datasets/processCapability.csv", options) + set.seed(2) + second <- runAnalysis("bayesianProcessCapabilityStudies", "datasets/processCapability.csv", options) + + expect_equal(.capabilityRows(first)$mean, .capabilityRows(second)$mean) + expect_equal(.capabilityRows(first)$sd, .capabilityRows(second)$sd) +}) + +## Plots #### + +test_that("Distribution and predictive plots are produced without error", { + options <- .bpcsOptions() + options$posteriorDistributionPlot <- TRUE + options$priorDistributionPlot <- TRUE + options$posteriorPredictiveDistributionPlot <- TRUE + options$priorPredictiveDistributionPlot <- TRUE + set.seed(1) + results <- runAnalysis("bayesianProcessCapabilityStudies", "datasets/processCapability.csv", options) + + expect_equal(results[["status"]], "complete") + for (base in c("posteriorDistributionPlot", "priorDistributionPlot", + "posteriorPredictiveDistributionPlot", "priorPredictiveDistributionPlot")) { + plotName <- results[["results"]][[base]][["data"]] + expect_true(!is.null(plotName), info = base) + } + expect_length(results[["state"]][["figures"]], 4) +}) + +## Interval table #### + +test_that("Interval table is produced without error", { + options <- .bpcsOptions() + options$intervalTable <- TRUE + set.seed(1) + results <- runAnalysis("bayesianProcessCapabilityStudies", "datasets/processCapability.csv", options) + + expect_equal(results[["status"]], "complete") + expect_true(length(results[["results"]][["bpcsIntervalTable"]][["data"]]) > 0) +}) + +## Readiness #### + +test_that("Analysis stays empty until the specification limits are set", { + options <- .bpcsOptions() + options$lowerSpecificationLimit <- FALSE + options$upperSpecificationLimit <- FALSE + options$target <- FALSE + set.seed(1) + results <- runAnalysis("bayesianProcessCapabilityStudies", "datasets/processCapability.csv", options) + + expect_equal(results[["status"]], "complete") + expect_length(.capabilityRows(results), 0) +}) From 2e9b4c1cc0358369e52f06179056a88350d6290c Mon Sep 17 00:00:00 2001 From: Julius Pfadt Date: Thu, 6 Aug 2026 14:44:56 +0200 Subject: [PATCH 06/14] fix: declare stats and R >= 4.4, add sequential analysis tests - stats:: is used for density/median/quantile but was undeclared - %||% at the mcmc fallback is base R only since 4.4.0, undeclared - drop qualityControl-measurement.svg, unreferenced until gauge r&R lands - cover the sequential analysis plots, previously untested, and pin the unimplemented "Posterior updating table" option so it is not forgotten Co-Authored-By: Claude Opus 5 (1M context) --- DESCRIPTION | 3 + inst/icons/qualityControl-measurement.svg | 175 ------------------ .../test-bayesianProcessCapabilityStudies.R | 30 +++ 3 files changed, 33 insertions(+), 175 deletions(-) delete mode 100644 inst/icons/qualityControl-measurement.svg diff --git a/DESCRIPTION b/DESCRIPTION index d19b06b..89db3f7 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -9,6 +9,8 @@ Maintainer: JASP Description: Bayesian counterparts to the quality control analyses, covering process capability and measurement systems analysis. License: GPL (>= 2) Encoding: UTF-8 +Depends: + R (>= 4.4.0) Imports: BayesTools, ggh4x, @@ -17,6 +19,7 @@ Imports: jaspBase, jaspGraphs, qc, + stats, tibble Suggests: testthat diff --git a/inst/icons/qualityControl-measurement.svg b/inst/icons/qualityControl-measurement.svg deleted file mode 100644 index 2aa5d3d..0000000 --- a/inst/icons/qualityControl-measurement.svg +++ /dev/null @@ -1,175 +0,0 @@ - - - -image/svg+xml - - - - - - - - - - \ No newline at end of file diff --git a/tests/testthat/test-bayesianProcessCapabilityStudies.R b/tests/testthat/test-bayesianProcessCapabilityStudies.R index ff24562..37b7e8d 100644 --- a/tests/testthat/test-bayesianProcessCapabilityStudies.R +++ b/tests/testthat/test-bayesianProcessCapabilityStudies.R @@ -138,6 +138,36 @@ test_that("Distribution and predictive plots are produced without error", { expect_length(results[["state"]][["figures"]], 4) }) +## Sequential analysis #### + +test_that("Sequential analysis plots are produced without error", { + # slow, the sequential analysis refits once per observation + options <- .bpcsOptions() + options$sequentialAnalysisPointEstimatePlot <- TRUE + options$sequentialAnalysisPointIntervalPlot <- TRUE + set.seed(1) + results <- runAnalysis("bayesianProcessCapabilityStudies", "datasets/processCapability.csv", options) + + expect_equal(results[["status"]], "complete") + expect_true(!is.null(results[["results"]][["sequentialAnalysisPointEstimatePlot"]][["data"]])) + expect_true(!is.null(results[["results"]][["sequentialAnalysisPointIntervalPlot"]][["data"]])) + expect_length(results[["state"]][["figures"]], 2) +}) + +test_that("Posterior updating table option is not implemented yet", { + # the qml ships a "Posterior updating table" checkbox (marked TODO) with no R + # implementation, so ticking it adds nothing. Guards against the option being + # quietly forgotten: delete this test when the table is implemented. + options <- .bpcsOptions() + options$sequentialAnalysisPointEstimatePlot <- TRUE + options$sequentialAnalysisUpdatingTable <- TRUE + set.seed(1) + results <- runAnalysis("bayesianProcessCapabilityStudies", "datasets/processCapability.csv", options) + + expect_equal(results[["status"]], "complete") + expect_false("sequentialAnalysisUpdatingTable" %in% names(results[["results"]])) +}) + ## Interval table #### test_that("Interval table is produced without error", { From 9d3d38dfdec7189da5cf3ad0f3661f93569075a0 Mon Sep 17 00:00:00 2001 From: Julius Pfadt Date: Thu, 6 Aug 2026 21:30:03 +0200 Subject: [PATCH 07/14] build: regenerate renv.lock for the module's actual dependencies the template lockfile carried 61 packages and none of qc, BayesTools, HDInterval or ggh4x, so renv::restore() could not have satisfied this module. regenerated to 129 packages against R 4.5.2, matching CI. - qc, jaspBase and jaspGraphs are pinned by commit sha, so the qc API drift that broke #414 cannot recur silently - fs, glue and posterior had been recorded against literal repository urls not present in the lockfile; all three are on CRAN at the same versions, so they are normalised to CRAN - RSPM is declared alongside CRAN, mirroring jaspQualityControl, so the ggh4x entry resolves jaspTools is deliberately absent, jasp-actions installs it separately. Co-Authored-By: Claude Opus 5 (1M context) --- renv.lock | 1217 +++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 1079 insertions(+), 138 deletions(-) diff --git a/renv.lock b/renv.lock index 17c464a..7bb95ef 100644 --- a/renv.lock +++ b/renv.lock @@ -1,7 +1,11 @@ { "R": { - "Version": "4.5.0", + "Version": "4.5.2", "Repositories": [ + { + "Name": "RSPM", + "URL": "https://packagemanager.posit.co/cran/2026-04-22" + }, { "Name": "CRAN", "URL": "https://cloud.r-project.org" @@ -9,24 +13,56 @@ ] }, "Packages": { - "MASS": { - "Package": "MASS", - "Version": "7.3-65", + "BH": { + "Package": "BH", + "Version": "1.90.0-1", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "87905902999e70199a81869ef4ccaf82" + }, + "BayesTools": { + "Package": "BayesTools", + "Version": "0.3.0", "Source": "Repository", "Repository": "CRAN", "Requirements": [ "R", - "grDevices", + "Rdpack", + "bridgesampling", + "coda", + "extraDistr", + "ggplot2", "graphics", - "methods", - "stats", - "utils" + "grid", + "mvtnorm", + "parallel", + "rlang", + "stats" ], - "Hash": "a41d0fc833ea756a1136b60a437efe26" + "Hash": "e68b5a1960e3f423701db43fc3115d6e" + }, + "Brobdingnag": { + "Package": "Brobdingnag", + "Version": "1.2-9", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "Matrix", + "R", + "methods" + ], + "Hash": "00077243042334c50b74cdfafe172870" + }, + "HDInterval": { + "Package": "HDInterval", + "Version": "0.2.4", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "b5b77433b286dd869ff33ee7fd5c545f" }, "Matrix": { "Package": "Matrix", - "Version": "1.7-3", + "Version": "1.7-4", "Source": "Repository", "Repository": "CRAN", "Requirements": [ @@ -39,7 +75,14 @@ "stats", "utils" ], - "Hash": "fb578c2b5d796882c60e9f770352f7c4" + "Hash": "7dbe8933065523bfc227027091bf2b1f" + }, + "QuickJSR": { + "Package": "QuickJSR", + "Version": "1.10.0", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "08b7b1ee36f1ad560fe72b1588a6f5d2" }, "R6": { "Package": "R6", @@ -63,14 +106,101 @@ }, "Rcpp": { "Package": "Rcpp", - "Version": "1.0.14", + "Version": "1.1.2", "Source": "Repository", "Repository": "CRAN", "Requirements": [ + "R", "methods", "utils" ], - "Hash": "e7bdd9ee90e96921ca8a0f1972d66682" + "Hash": "f481f89daa906a34eab8ef8658c8a89c" + }, + "RcppArmadillo": { + "Package": "RcppArmadillo", + "Version": "15.4.2-1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "Rcpp", + "methods", + "stats", + "utils" + ], + "Hash": "f7694f9bd161314b273073279998a69b" + }, + "RcppEigen": { + "Package": "RcppEigen", + "Version": "0.3.4.0.2", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "Rcpp", + "stats", + "utils" + ], + "Hash": "4ac8e423216b8b70cb9653d1b3f71eb9" + }, + "RcppParallel": { + "Package": "RcppParallel", + "Version": "6.2.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R" + ], + "Hash": "ae97343f2392836689f02881879ff787" + }, + "Rdpack": { + "Package": "Rdpack", + "Version": "2.6.6", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "methods", + "rbibutils", + "tools", + "utils" + ], + "Hash": "9aafb16a721acbb1f793e7b9e26de471" + }, + "S7": { + "Package": "S7", + "Version": "0.2.2", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "utils" + ], + "Hash": "6a72e94a8c9be4ef719af3aa3628f2dc" + }, + "StanHeaders": { + "Package": "StanHeaders", + "Version": "2.32.10", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "RcppEigen", + "RcppParallel" + ], + "Hash": "c35dc5b81d7ffb1018aa090dff364ecb" + }, + "abind": { + "Package": "abind", + "Version": "1.4-8", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "methods", + "utils" + ], + "Hash": "2288423bb0f20a457800d7fc47f6aa54" }, "askpass": { "Package": "askpass", @@ -82,59 +212,196 @@ ], "Hash": "c39f4155b3ceb1a9a2799d700fbd4b6a" }, + "backports": { + "Package": "backports", + "Version": "1.5.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R" + ], + "Hash": "e3e65442a2749b59692220f368f46c46" + }, "base64enc": { "Package": "base64enc", - "Version": "0.1-3", + "Version": "0.1-6", "Source": "Repository", "Repository": "CRAN", "Requirements": [ "R" ], - "Hash": "543776ae6848fde2f48ff3816d0628bc" + "Hash": "5edb675b7baa6e9a0d86dd2c28de1676" + }, + "bridgesampling": { + "Package": "bridgesampling", + "Version": "1.2-1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "Brobdingnag", + "Matrix", + "R", + "coda", + "methods", + "mvtnorm", + "parallel", + "scales", + "stringr", + "utils" + ], + "Hash": "da64ef771aeb8daeaed65ef1e5ea419a" + }, + "bslib": { + "Package": "bslib", + "Version": "0.12.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "base64enc", + "cachem", + "fastmap", + "grDevices", + "htmltools", + "jquerylib", + "jsonlite", + "lifecycle", + "memoise", + "mime", + "rlang", + "sass" + ], + "Hash": "b2d0b0a17142ed4858f252d88eb56223" + }, + "cachem": { + "Package": "cachem", + "Version": "1.1.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "fastmap", + "rlang" + ], + "Hash": "cd9a672193789068eb5a2aad65a0dedf" }, "callr": { "Package": "callr", - "Version": "3.7.6", + "Version": "3.8.0", "Source": "Repository", "Repository": "CRAN", "Requirements": [ "R", "R6", + "otel", "processx", "utils" ], - "Hash": "d7e13f49c19103ece9e58ad2d83a7354" + "Hash": "cb2799e0a02c2ac4d0395b0e29bb6799" + }, + "checkmate": { + "Package": "checkmate", + "Version": "2.3.4", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "backports", + "utils" + ], + "Hash": "86cbe221fc19b56242f6c3eb0d4c5ab5" }, "cli": { "Package": "cli", - "Version": "3.6.5", + "Version": "3.6.6", "Source": "Repository", "Repository": "CRAN", "Requirements": [ "R", "utils" ], - "Hash": "16850760556401a2eeb27d39bd11c9cb" + "Hash": "a73d822b669d443ff8de6928f9c49850" + }, + "coda": { + "Package": "coda", + "Version": "0.19-4.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "lattice" + ], + "Hash": "af436915c590afc6fffc3ce3a5be1569" }, "codetools": { "Package": "codetools", - "Version": "0.2-19", + "Version": "0.2-20", "Source": "Repository", "Repository": "CRAN", "Requirements": [ "R" ], - "Hash": "c089a619a7fae175d149d89164f8c7d8" + "Hash": "61e097f35917d342622f21cdc79c256e" + }, + "commonmark": { + "Package": "commonmark", + "Version": "2.0.0", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "8cba62334c1088d21689d353a7e87663" }, "cpp11": { "Package": "cpp11", - "Version": "0.5.2", + "Version": "0.5.5", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R" + ], + "Hash": "20ecb9105a3fb48a8390919abc3b7e90" + }, + "crosstalk": { + "Package": "crosstalk", + "Version": "1.2.2", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R6", + "htmltools", + "jsonlite", + "lazyeval" + ], + "Hash": "8b008bc619e0bbebb5646b3d37efdad1" + }, + "cubature": { + "Package": "cubature", + "Version": "2.1.4-1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "Rcpp" + ], + "Hash": "59e1f90dd605a6258a7686686d08f732" + }, + "curl": { + "Package": "curl", + "Version": "7.1.0", "Source": "Repository", "Repository": "CRAN", "Requirements": [ "R" ], - "Hash": "2720e3fd3dad08f34b19b56b3d6f073d" + "Hash": "2e004ed19964915a8faf48a574439be9" + }, + "data.table": { + "Package": "data.table", + "Version": "1.18.4", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "methods" + ], + "Hash": "da9ddede68a6f6e5d3098c0ab81b6e1f" }, "desc": { "Package": "desc", @@ -151,14 +418,77 @@ }, "digest": { "Package": "digest", - "Version": "0.6.37", + "Version": "0.6.39", "Source": "Repository", "Repository": "CRAN", "Requirements": [ "R", "utils" ], - "Hash": "33698c4b3127fc9f506654607fb73676" + "Hash": "d18028e978a88b2b16ef8d400cb49adf" + }, + "distributional": { + "Package": "distributional", + "Version": "0.8.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "generics", + "lifecycle", + "numDeriv", + "pillar", + "rlang", + "stats", + "utils", + "vctrs" + ], + "Hash": "72f50cd7f9ae374a323c832ab6c25171" + }, + "dplyr": { + "Package": "dplyr", + "Version": "1.2.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "R6", + "cli", + "generics", + "glue", + "lifecycle", + "magrittr", + "methods", + "pillar", + "rlang", + "tibble", + "tidyselect", + "utils", + "vctrs" + ], + "Hash": "d71f190466b9496cf8543c76641be5cf" + }, + "evaluate": { + "Package": "evaluate", + "Version": "1.0.5", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R" + ], + "Hash": "94cf2c54237f6841cee68e3ba4ab5a14" + }, + "extraDistr": { + "Package": "extraDistr", + "Version": "1.10.0.5", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "Rcpp", + "RcppArmadillo" + ], + "Hash": "678411b96db4b871bd98336049adc566" }, "farver": { "Package": "farver", @@ -194,6 +524,18 @@ ], "Hash": "f918c5e723f86f409912104d5b7a71d6" }, + "fontawesome": { + "Package": "fontawesome", + "Version": "0.5.3", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "htmltools", + "rlang" + ], + "Hash": "bd1297f9b5b1fc1372d19e2c4cd82215" + }, "fontquiver": { "Package": "fontquiver", "Version": "0.2.1", @@ -206,9 +548,20 @@ ], "Hash": "fc0f4226379e451057d55419fd31761e" }, + "fs": { + "Package": "fs", + "Version": "2.1.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "methods" + ], + "Hash": "09278623bca442bc53b0940ffa2f6d87" + }, "gdtools": { "Package": "gdtools", - "Version": "0.4.2", + "Version": "0.5.1", "Source": "Repository", "Repository": "CRAN", "Requirements": [ @@ -219,47 +572,89 @@ "systemfonts", "tools" ], - "Hash": "d022502651388a6bb8545988514d8780" + "Hash": "bca589967c4b7360ced3c08c69a96787" + }, + "generics": { + "Package": "generics", + "Version": "0.1.4", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "methods" + ], + "Hash": "4b29bf698d0c7bdb9f1e4976e7ade41d" + }, + "ggh4x": { + "Package": "ggh4x", + "Version": "0.3.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "S7", + "cli", + "ggplot2", + "grid", + "gtable", + "lifecycle", + "rlang", + "scales", + "stats", + "vctrs" + ], + "Hash": "024c1c1d5894bf313a1625b167984f2e" }, "ggplot2": { "Package": "ggplot2", - "Version": "3.5.2", + "Version": "4.0.3", "Source": "Repository", "Repository": "CRAN", "Requirements": [ - "MASS", "R", + "S7", "cli", - "glue", "grDevices", "grid", "gtable", "isoband", "lifecycle", - "mgcv", "rlang", "scales", "stats", - "tibble", "vctrs", "withr" ], - "Hash": "7ad64861e028a777d7d67ff83231b548" + "Hash": "98520fe6b2745c466dca8e46aaa86242" + }, + "ggtext": { + "Package": "ggtext", + "Version": "0.1.2", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "ggplot2", + "grid", + "gridtext", + "rlang", + "scales" + ], + "Hash": "c5ba8f5056487403a299b91984be86ca" }, "glue": { "Package": "glue", - "Version": "1.8.0", + "Version": "1.8.1", "Source": "Repository", "Repository": "CRAN", "Requirements": [ "R", "methods" ], - "Hash": "5899f1eaa825580172bb56c08266f37c" + "Hash": "f8122473e9a49e00d0642f78235ca5e3" }, "gridExtra": { "Package": "gridExtra", - "Version": "2.3", + "Version": "2.3.1", "Source": "Repository", "Repository": "CRAN", "Requirements": [ @@ -269,7 +664,7 @@ "gtable", "utils" ], - "Hash": "7d7f283939f563670a697165b2cf5560" + "Hash": "4e172eb9a8e2e01e0494ea230b73f068" }, "gridGraphics": { "Package": "gridGraphics", @@ -283,6 +678,36 @@ ], "Hash": "5b79228594f02385d4df4979284879ae" }, + "gridtext": { + "Package": "gridtext", + "Version": "0.1.6", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "Rcpp", + "curl", + "grDevices", + "grid", + "jpeg", + "markdown", + "png", + "rlang", + "stringr", + "xml2" + ], + "Hash": "44ba01d9fa1dda584a3f880c212dcd93" + }, + "gsl": { + "Package": "gsl", + "Version": "2.1-9", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R" + ], + "Hash": "6c2e67b032a0a3dba59dfc7acb71bb64" + }, "gtable": { "Package": "gtable", "Version": "0.3.6", @@ -299,9 +724,20 @@ ], "Hash": "de949855009e2d4d0e52a844e30617ae" }, + "highr": { + "Package": "highr", + "Version": "0.12", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "xfun" + ], + "Hash": "2a2f862ade01a56dcbcd60944de11255" + }, "htmltools": { "Package": "htmltools", - "Version": "0.5.8.1", + "Version": "0.5.9", "Source": "Repository", "Repository": "CRAN", "Requirements": [ @@ -313,30 +749,73 @@ "rlang", "utils" ], - "Hash": "81d371a9cc60640e74e4ab6ac46dcedc" + "Hash": "102298e238c14eb830cc4b5edd23c3e8" + }, + "htmlwidgets": { + "Package": "htmlwidgets", + "Version": "1.6.4", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "grDevices", + "htmltools", + "jsonlite", + "knitr", + "rmarkdown", + "yaml" + ], + "Hash": "04291cc45198225444a397606810ac37" + }, + "httr": { + "Package": "httr", + "Version": "1.4.8", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "R6", + "curl", + "jsonlite", + "mime", + "openssl" + ], + "Hash": "10bcae7793493db63a6872096132e10c" + }, + "inline": { + "Package": "inline", + "Version": "0.3.21", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "methods" + ], + "Hash": "a816db522447eb5ca56e7d4e6e82e4eb" }, "isoband": { "Package": "isoband", - "Version": "0.2.7", + "Version": "0.3.0", "Source": "Repository", "Repository": "CRAN", "Requirements": [ + "cli", + "cpp11", "grid", + "rlang", "utils" ], - "Hash": "0080607b4a1a7b28979aecef976d8bc2" + "Hash": "0f9a864bbd7ce0232ad05cb76249cc1a" }, "jaspBase": { "Package": "jaspBase", - "Version": "0.20.0", + "Version": "0.20.4", "Source": "GitHub", "RemoteType": "github", + "Remotes": "jasp-stats/jaspGraphs", "RemoteHost": "api.github.com", "RemoteUsername": "jasp-stats", "RemoteRepo": "jaspBase", "RemoteRef": "master", - "RemoteSha": "9dd637722c25bf0ea035ccaabf58ef9faf36b852", - "Remotes": "jasp-stats/jaspGraphs", + "RemoteSha": "763cb835a7589b72da079b95bbf9491abad4fab3", "Requirements": [ "R6", "Rcpp", @@ -361,7 +840,7 @@ "systemfonts", "withr" ], - "Hash": "75eac9c2c157d37e5f503baebdc54be3" + "Hash": "9932dbefb2240532846b935be759503b" }, "jaspGraphs": { "Package": "jaspGraphs", @@ -372,144 +851,278 @@ "RemoteUsername": "jasp-stats", "RemoteRepo": "jaspGraphs", "RemoteRef": "master", - "RemoteSha": "c884a4239590cdb08c1f18cbeaad9107395425aa", + "RemoteSha": "23cdda0d795329eb99a2659bf6f325a9194ff292", "Requirements": [ "R6", - "RColorBrewer", + "cli", "ggplot2", "gridExtra", "gtable", + "htmlwidgets", "jsonlite", "lifecycle", + "plotly", "rlang", - "scales", - "viridisLite" + "scales" + ], + "Hash": "482289fc59758164e53c1866cc19fd9b" + }, + "jpeg": { + "Package": "jpeg", + "Version": "0.1-11", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R" + ], + "Hash": "c23ab23c370d1ce3a7a80d8c0bdfa105" + }, + "jquerylib": { + "Package": "jquerylib", + "Version": "0.1.4", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "htmltools" + ], + "Hash": "5aab57a3bd297eee1c1d862735972182" + }, + "jsonlite": { + "Package": "jsonlite", + "Version": "2.0.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "methods" + ], + "Hash": "b0776f526d36d8bd4a3344a88fe165c4" + }, + "knitr": { + "Package": "knitr", + "Version": "1.51", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "evaluate", + "highr", + "methods", + "tools", + "xfun", + "yaml" + ], + "Hash": "27682babb50f03b6eb7939ea69ec79ca" + }, + "labeling": { + "Package": "labeling", + "Version": "0.4.3", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "graphics", + "stats" + ], + "Hash": "b64ec208ac5bc1852b285f665d6368b3" + }, + "later": { + "Package": "later", + "Version": "1.4.8", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "Rcpp", + "rlang" + ], + "Hash": "824c180e69b9be79ab96a985e233c470" + }, + "lattice": { + "Package": "lattice", + "Version": "0.22-7", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "grDevices", + "graphics", + "grid", + "stats", + "utils" + ], + "Hash": "934f30aea6442867f57a610034ab06d3" + }, + "lazyeval": { + "Package": "lazyeval", + "Version": "0.2.3", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "rlang" + ], + "Hash": "2757e46c2633dd5854f62615df70d0d7" + }, + "lifecycle": { + "Package": "lifecycle", + "Version": "1.0.5", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "cli", + "rlang" + ], + "Hash": "36dbfe4fba6c064db50a671a90297c85" + }, + "litedown": { + "Package": "litedown", + "Version": "0.10", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "commonmark", + "utils", + "xfun" ], - "Hash": "a396da19666830203898f153a70816d2" + "Hash": "0bb78ad1932aa147c433f16183382219" }, - "jsonlite": { - "Package": "jsonlite", - "Version": "2.0.0", + "loo": { + "Package": "loo", + "Version": "2.10.1", "Source": "Repository", "Repository": "CRAN", "Requirements": [ - "methods" + "R", + "checkmate", + "matrixStats", + "parallel", + "posterior", + "stats" ], - "Hash": "b0776f526d36d8bd4a3344a88fe165c4" + "Hash": "557142a63f951e40db1af9f907365cfc" }, - "labeling": { - "Package": "labeling", - "Version": "0.4.3", + "magrittr": { + "Package": "magrittr", + "Version": "2.0.5", "Source": "Repository", "Repository": "CRAN", "Requirements": [ - "graphics", - "stats" + "R" ], - "Hash": "b64ec208ac5bc1852b285f665d6368b3" + "Hash": "665e77ab6e5f37a7913226d40b324e37" }, - "lattice": { - "Package": "lattice", - "Version": "0.22-5", + "markdown": { + "Package": "markdown", + "Version": "2.0", "Source": "Repository", "Repository": "CRAN", "Requirements": [ "R", - "grDevices", - "graphics", - "grid", - "stats", - "utils" + "litedown", + "utils", + "xfun" ], - "Hash": "7c5e89f04e72d6611c77451f6331a091" + "Hash": "b4349847250b103bbbb8bc6819c4fbca" }, - "lifecycle": { - "Package": "lifecycle", - "Version": "1.0.4", + "matrixStats": { + "Package": "matrixStats", + "Version": "1.5.0", "Source": "Repository", "Repository": "CRAN", "Requirements": [ - "R", - "cli", - "glue", + "R" + ], + "Hash": "9fd316b52ac8c24fef4c67fdd646e965" + }, + "memoise": { + "Package": "memoise", + "Version": "2.0.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "cachem", "rlang" ], - "Hash": "b8552d117e1b808b09a832f589b79035" + "Hash": "e2817ccf4a065c5d9d7f2cfbe7c1d78c" }, - "magrittr": { - "Package": "magrittr", - "Version": "2.0.3", + "mime": { + "Package": "mime", + "Version": "0.13", "Source": "Repository", "Repository": "CRAN", "Requirements": [ - "R" + "tools" ], - "Hash": "7ce2733a9826b3aeb1775d56fd305472" + "Hash": "0ec19f34c72fab674d8f2b4b1c6410e1" }, - "mgcv": { - "Package": "mgcv", - "Version": "1.9-1", + "mvtnorm": { + "Package": "mvtnorm", + "Version": "1.4-2", "Source": "Repository", "Repository": "CRAN", "Requirements": [ - "Matrix", "R", - "graphics", - "methods", - "nlme", - "splines", "stats", "utils" ], - "Hash": "110ee9d83b496279960e162ac97764ce" + "Hash": "f12b9432ec251666c49e7d01f1aff3c7" }, - "nlme": { - "Package": "nlme", - "Version": "3.1-168", + "numDeriv": { + "Package": "numDeriv", + "Version": "2016.8-1.1", "Source": "Repository", "Repository": "CRAN", "Requirements": [ - "R", - "graphics", - "lattice", - "stats", - "utils" + "R" ], - "Hash": "b1d2ea08d5d392831fbc32c872362b06" + "Hash": "df58958f293b166e4ab885ebcad90e02" }, "officer": { "Package": "officer", - "Version": "0.6.10", + "Version": "0.7.6", "Source": "Repository", "Repository": "CRAN", "Requirements": [ "R6", "cli", + "dplyr", "grDevices", "graphics", "openssl", "ragg", "stats", + "tidyr", "utils", "uuid", "xml2", "zip" ], - "Hash": "d8673b646d055738b68e8c54acabe8cf" + "Hash": "8ecbb62f412a5939760ea7eb565acc77" }, "openssl": { "Package": "openssl", - "Version": "2.3.3", + "Version": "2.4.2", "Source": "Repository", "Repository": "CRAN", "Requirements": [ "askpass" ], - "Hash": "05ce1ed077e8c97fbb3ec1cb078f1159" + "Hash": "6994d1c3ea954f29de6aeca5da95c99d" + }, + "otel": { + "Package": "otel", + "Version": "0.2.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R" + ], + "Hash": "627d6993db1043703c0b084fa432f21f" }, "pillar": { "Package": "pillar", - "Version": "1.10.2", + "Version": "1.11.1", "Source": "Repository", "Repository": "CRAN", "Requirements": [ @@ -521,7 +1134,7 @@ "utils", "vctrs" ], - "Hash": "1098920a19b5cd5a15bacdc74a89979d" + "Hash": "1395e64f2689ffd503657778e810cee2" }, "pkgbuild": { "Package": "pkgbuild", @@ -548,6 +1161,37 @@ ], "Hash": "01f28d4278f15c76cddbea05899c5d6f" }, + "plotly": { + "Package": "plotly", + "Version": "4.12.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "RColorBrewer", + "base64enc", + "crosstalk", + "data.table", + "digest", + "dplyr", + "ggplot2", + "htmltools", + "htmlwidgets", + "httr", + "jsonlite", + "magrittr", + "promises", + "purrr", + "rlang", + "scales", + "tibble", + "tidyr", + "tools", + "vctrs", + "viridisLite" + ], + "Hash": "3609e47f0d9713dfd7f68bd6e731e27b" + }, "plyr": { "Package": "plyr", "Version": "1.8.9", @@ -559,9 +1203,45 @@ ], "Hash": "6b8177fd19982f0020743fadbfdbd933" }, + "png": { + "Package": "png", + "Version": "0.1-9", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R" + ], + "Hash": "961c433971606243a724eb19b1075e60" + }, + "posterior": { + "Package": "posterior", + "Version": "1.7.0", + "Source": "Repository", + "Repository": "CRAN", + "RemoteType": "repository", + "RemoteUrl": "https://github.com/stan-dev/posterior", + "RemoteRef": "HEAD", + "RemoteSha": "727ac83cb52babb6eb54bdd1ce15f90439626945", + "Requirements": [ + "R", + "abind", + "checkmate", + "distributional", + "matrixStats", + "methods", + "parallel", + "pillar", + "rlang", + "stats", + "tensorA", + "tibble", + "vctrs" + ], + "Hash": "a0f7fccb443ef5069577686efbbbd79b" + }, "processx": { "Package": "processx", - "Version": "3.8.6", + "Version": "3.9.0", "Source": "Repository", "Repository": "CRAN", "Requirements": [ @@ -570,29 +1250,114 @@ "ps", "utils" ], - "Hash": "720161b280b0a35f4d1490ead2fe81d0" + "Hash": "188d6caf38e81bf15b5e918726a6fd3d" + }, + "promises": { + "Package": "promises", + "Version": "1.5.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "R6", + "fastmap", + "later", + "lifecycle", + "magrittr", + "otel", + "rlang" + ], + "Hash": "62cb899ed5fff70d4e918ec1b762bf7c" }, "ps": { "Package": "ps", - "Version": "1.9.1", + "Version": "1.9.3", "Source": "Repository", "Repository": "CRAN", "Requirements": [ "R", "utils" ], - "Hash": "093688087b0bacce6ba2f661f36328e2" + "Hash": "83e7e486c434e7acc71766dfc20adf2b" + }, + "purrr": { + "Package": "purrr", + "Version": "1.2.2", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "cli", + "lifecycle", + "magrittr", + "rlang", + "vctrs" + ], + "Hash": "0a35605539b085a4828ec55ad973fe60" + }, + "qc": { + "Package": "qc", + "Version": "0.0.0.9000", + "Source": "GitHub", + "RemoteType": "github", + "RemoteHost": "api.github.com", + "RemoteUsername": "FBartos", + "RemoteRepo": "qc", + "RemoteRef": "main", + "RemoteSha": "4204e03b4f9e885dbc50bc142a5ff3ec6637281f", + "Requirements": [ + "BH", + "BayesTools", + "HDInterval", + "R", + "Rcpp", + "RcppEigen", + "RcppParallel", + "StanHeaders", + "cubature", + "ggplot2", + "ggtext", + "gsl", + "methods", + "rstan", + "rstantools", + "tibble", + "vctrs" + ], + "Hash": "dda836a23ca2b95e52f339c34fbace88" }, "ragg": { "Package": "ragg", - "Version": "1.4.0", + "Version": "1.5.2", "Source": "Repository", "Repository": "CRAN", "Requirements": [ "systemfonts", "textshaping" ], - "Hash": "1591adde9ce8ff7de58072e4a32b66ce" + "Hash": "c06b460d55dd27458977e417e8ff02c7" + }, + "rappdirs": { + "Package": "rappdirs", + "Version": "0.3.4", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R" + ], + "Hash": "f146a5bfc94db048309712535d4d0aee" + }, + "rbibutils": { + "Package": "rbibutils", + "Version": "2.4.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "tools", + "utils" + ], + "Hash": "92dd8158e4025e954fd06fcda0def88b" }, "renv": { "Package": "renv", @@ -606,18 +1371,79 @@ }, "rlang": { "Package": "rlang", - "Version": "1.1.6", + "Version": "1.3.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "utils" + ], + "Hash": "8d05afdb0b0dd5ef01b306db289fe21f" + }, + "rmarkdown": { + "Package": "rmarkdown", + "Version": "2.31", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "bslib", + "evaluate", + "fontawesome", + "htmltools", + "jquerylib", + "jsonlite", + "knitr", + "methods", + "tinytex", + "tools", + "utils", + "xfun", + "yaml" + ], + "Hash": "f34039d57d861d2869cbf9be813ed08e" + }, + "rstan": { + "Package": "rstan", + "Version": "2.32.7", "Source": "Repository", "Repository": "CRAN", "Requirements": [ + "BH", + "QuickJSR", "R", + "Rcpp", + "RcppEigen", + "RcppParallel", + "StanHeaders", + "ggplot2", + "gridExtra", + "inline", + "loo", + "methods", + "pkgbuild", + "stats4" + ], + "Hash": "5f47b80f0db40503697eef138a31a6ef" + }, + "rstantools": { + "Package": "rstantools", + "Version": "2.7.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "QuickJSR", + "Rcpp", + "RcppParallel", + "desc", + "stats", "utils" ], - "Hash": "892124978869b74935dc3934c42bfe5a" + "Hash": "cc23398b8a73a7f5a1d893a4344f9fcc" }, "rvg": { "Package": "rvg", - "Version": "0.3.5", + "Version": "0.4.2", "Source": "Repository", "Repository": "CRAN", "Requirements": [ @@ -627,9 +1453,24 @@ "grDevices", "officer", "rlang", + "systemfonts", "xml2" ], - "Hash": "5205600ad4a5632089c51434b30db883" + "Hash": "e31ff9b1a3e17e45274109aa917a2aa9" + }, + "sass": { + "Package": "sass", + "Version": "0.4.10", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R6", + "fs", + "htmltools", + "rappdirs", + "rlang" + ], + "Hash": "3fb78d066fb92299b1d13f6a7c9a90a8" }, "scales": { "Package": "scales", @@ -652,7 +1493,7 @@ }, "stringi": { "Package": "stringi", - "Version": "1.8.7", + "Version": "1.8.9", "Source": "Repository", "Repository": "CRAN", "Requirements": [ @@ -661,11 +1502,28 @@ "tools", "utils" ], - "Hash": "2b56088e23bdd58f89aebf43a0913457" + "Hash": "67f6c2a9e67d08e1e419ff89ad135444" + }, + "stringr": { + "Package": "stringr", + "Version": "1.6.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "cli", + "glue", + "lifecycle", + "magrittr", + "rlang", + "stringi", + "vctrs" + ], + "Hash": "d47392652eedc68bf916657347ff2526" }, "svglite": { "Package": "svglite", - "Version": "2.2.1", + "Version": "2.2.2", "Source": "Repository", "Repository": "CRAN", "Requirements": [ @@ -678,7 +1536,7 @@ "systemfonts", "textshaping" ], - "Hash": "a8a754856a1b29a24cbe269b8e03989a" + "Hash": "40b8a31d6734e45bbb44f241afeb4903" }, "sys": { "Package": "sys", @@ -689,7 +1547,7 @@ }, "systemfonts": { "Package": "systemfonts", - "Version": "1.2.3", + "Version": "1.3.2", "Source": "Repository", "Repository": "CRAN", "Requirements": [ @@ -702,11 +1560,22 @@ "tools", "utils" ], - "Hash": "fe31683d2c6fd9a5724bcdf8ed44ded9" + "Hash": "1f930828d6590af5da47b3ab4fe161e6" + }, + "tensorA": { + "Package": "tensorA", + "Version": "0.36.2.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "stats" + ], + "Hash": "0d587599172f2ffda2c09cb6b854e0e5" }, "textshaping": { "Package": "textshaping", - "Version": "1.0.1", + "Version": "1.0.5", "Source": "Repository", "Repository": "CRAN", "Requirements": [ @@ -718,11 +1587,11 @@ "systemfonts", "utils" ], - "Hash": "75b5813527f4154cb467e4cf60911333" + "Hash": "addb750a886a2ac415dea6f8068867de" }, "tibble": { "Package": "tibble", - "Version": "3.3.0", + "Version": "3.3.1", "Source": "Repository", "Repository": "CRAN", "Requirements": [ @@ -737,7 +1606,56 @@ "utils", "vctrs" ], - "Hash": "784b27d0801c3829de602105757b2cd7" + "Hash": "c55df870972551cac674b50cadb2d51f" + }, + "tidyr": { + "Package": "tidyr", + "Version": "1.3.2", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "cli", + "cpp11", + "dplyr", + "glue", + "lifecycle", + "magrittr", + "purrr", + "rlang", + "stringr", + "tibble", + "tidyselect", + "utils", + "vctrs" + ], + "Hash": "a4fa2f5876396f04814cb9d8d9ab89e9" + }, + "tidyselect": { + "Package": "tidyselect", + "Version": "1.2.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "cli", + "glue", + "lifecycle", + "rlang", + "vctrs", + "withr" + ], + "Hash": "829f27b9c4919c16b593794a6344d6c0" + }, + "tinytex": { + "Package": "tinytex", + "Version": "0.60", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "xfun" + ], + "Hash": "263651b52279eaa7835e44aa32f9b754" }, "utf8": { "Package": "utf8", @@ -751,17 +1669,17 @@ }, "uuid": { "Package": "uuid", - "Version": "1.2-1", + "Version": "1.2-2", "Source": "Repository", "Repository": "CRAN", "Requirements": [ "R" ], - "Hash": "34e965e62a41fcafb1ca60e9b142085b" + "Hash": "528fc9e90d70a6a115e21164f37b2c64" }, "vctrs": { "Package": "vctrs", - "Version": "0.6.5", + "Version": "0.7.3", "Source": "Repository", "Repository": "CRAN", "Requirements": [ @@ -771,21 +1689,21 @@ "lifecycle", "rlang" ], - "Hash": "c03fa420630029418f7e6da3667aac4a" + "Hash": "2dcde2d30d3ad67bf1d3a37177457b87" }, "viridisLite": { "Package": "viridisLite", - "Version": "0.4.2", + "Version": "0.4.3", "Source": "Repository", "Repository": "CRAN", "Requirements": [ "R" ], - "Hash": "c826c7c4241b6fc89ff55aaea3fa7491" + "Hash": "9380d36888b72faf5ae6c22b44703867" }, "withr": { "Package": "withr", - "Version": "3.0.2", + "Version": "3.0.3", "Source": "Repository", "Repository": "CRAN", "Requirements": [ @@ -793,11 +1711,24 @@ "grDevices", "graphics" ], - "Hash": "cc2d62c76458d425210d1eb1478b30b4" + "Hash": "d979712ec72df779bc2d30bcc5d0d541" + }, + "xfun": { + "Package": "xfun", + "Version": "0.60", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "grDevices", + "stats", + "tools" + ], + "Hash": "8304c2894061f6ae062996f09dd1528e" }, "xml2": { "Package": "xml2", - "Version": "1.3.8", + "Version": "1.6.0", "Source": "Repository", "Repository": "CRAN", "Requirements": [ @@ -806,14 +1737,24 @@ "methods", "rlang" ], - "Hash": "f5130b2f3d461964bac93cc618013231" + "Hash": "568fe669c645b2007e4e8fcf5cde40e7" + }, + "yaml": { + "Package": "yaml", + "Version": "2.3.12", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "7cd77cb32abd9220d744307e9fc94ffb" }, "zip": { "Package": "zip", - "Version": "2.3.3", + "Version": "3.0.2", "Source": "Repository", "Repository": "CRAN", - "Hash": "6ebe4b1dc74c3e50e74e316323629583" + "Requirements": [ + "cli" + ], + "Hash": "5807bea03035bfdd7535c1d3eb258cb0" } } } From c7bb71a8d2fc02226803168739feb61859e25c18 Mon Sep 17 00:00:00 2001 From: Julius Pfadt Date: Thu, 6 Aug 2026 22:15:49 +0200 Subject: [PATCH 08/14] ci: enable unit tests and add build-bundle and coverage workflows the template ships unittests.yml with its push/pull_request triggers commented out behind workflow_dispatch, so nothing ran on this PR. enabled them, with renv.lock added to the watched paths. build-bundle.yml is needed to prove the Stan-compiled qc dependency bundles on windows, linux and both macOS targets, which is the open risk on this module. test-coverage.yml skips drafts by design. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/build-bundle.yml | 23 +++++++++++++++++++++++ .github/workflows/test-coverage.yml | 21 +++++++++++++++++++++ .github/workflows/unittests.yml | 15 ++++++--------- 3 files changed, 50 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/build-bundle.yml create mode 100644 .github/workflows/test-coverage.yml diff --git a/.github/workflows/build-bundle.yml b/.github/workflows/build-bundle.yml new file mode 100644 index 0000000..d45d1f1 --- /dev/null +++ b/.github/workflows/build-bundle.yml @@ -0,0 +1,23 @@ +name: Build JASP Module on PR Comment + +on: + issue_comment: + types: [created] + +permissions: + pull-requests: write + contents: read + +jobs: + call-build-module: + # Only run if it's a PR, starts with 'build', and user has permissions + if: > + github.event.issue.pull_request && + startsWith(github.event.comment.body, 'build') && + contains(fromJson('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association) + + # Calls the reusable workflow from your central repo + uses: jasp-stats/jasp-actions/.github/workflows/build-bundle.yml@v1 + + # Passes the local repository's GITHUB_TOKEN down so the central action can comment and checkout code + secrets: inherit diff --git a/.github/workflows/test-coverage.yml b/.github/workflows/test-coverage.yml new file mode 100644 index 0000000..426887a --- /dev/null +++ b/.github/workflows/test-coverage.yml @@ -0,0 +1,21 @@ +on: + push: + branches: + - master # Essential for Codecov baseline + paths: ['**.R', 'tests/**', '**.c', '**.cpp', '**.h', '**.hpp', 'DESCRIPTION', 'NAMESPACE', 'MAKEVARS', 'MAKEVARS.win', '**.yml'] + + pull_request: + # Triggers when a PR is opened, updated, or marked ready for review + types: [opened, synchronize, reopened, ready_for_review] + paths: ['**.R', 'tests/**', '**.c', '**.cpp', '**.h', '**.hpp', 'DESCRIPTION', 'NAMESPACE', 'MAKEVARS', 'MAKEVARS.win'] + +jobs: + coverage: + # Run if it is a push (merge) OR if the PR is NOT a draft + if: github.event_name == 'push' || github.event.pull_request.draft == false + + uses: jasp-stats/jasp-actions/.github/workflows/coverage.yml@master + with: + needs_JAGS: false + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index d175855..2954c49 100644 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -1,15 +1,12 @@ on: + push: + paths: ['**.R', 'tests/**', '**.Rd', '**.c', '**.cpp', '**.h', '**.hpp', 'DESCRIPTION', 'NAMESPACE', 'MAKEVARS', 'MAKEVARS.win', '**.yml', 'renv.lock'] + pull_request: + paths: ['**.R', 'tests/**', '**.Rd', '**.c', '**.cpp', '**.h', '**.hpp', 'DESCRIPTION', 'NAMESPACE', 'MAKEVARS', 'MAKEVARS.win', '**.yml', 'renv.lock'] + schedule: + - cron: '13 12 * * 1-5' workflow_dispatch: -# once the module takes shape you may want to uncomment these lines -#on: -# push: -# paths: ['**.R', 'tests/**', '**.Rd', '**.c', '**.cpp', '**.h', '**.hpp', 'DESCRIPTION', 'NAMESPACE', 'MAKEVARS', 'MAKEVARS.win', '**.yml'] -# pull_request: -# paths: ['**.R', 'tests/**', '**.Rd', '**.c', '**.cpp', '**.h', '**.hpp', 'DESCRIPTION', 'NAMESPACE', 'MAKEVARS', 'MAKEVARS.win'] -# schedule: -# - cron: '13 12 * * 1-5' - name: unit-tests jobs: From c9c86ae190a817187f1bfd53e15a9ef1ebf1e9fd Mon Sep 17 00:00:00 2001 From: Julius Pfadt Date: Tue, 18 Aug 2026 15:52:10 +0200 Subject: [PATCH 09/14] chore: add JASP agent instructions for Claude, Codex, and Copilot Copied from jasp-agent-instructions (3f819da): shared rules, skills, and the mcp-server.R/session_startup.R bootstrap, in all three platform layouts. Machine-specific MCP configs (.mcp.json, .vscode/, settings.local.json) are gitignored; agent dirs and AGENTS.md are added to .Rbuildignore. Co-Authored-By: Claude Opus 5 (1M context) --- .Rbuildignore | 5 + .agents/skills/fix-debug-analysis/SKILL.md | 436 ++++++++++++++++++ .claude/CLAUDE.md | 275 +++++++++++ .claude/README.md | 125 +++++ .claude/hooks/block-test-edits.js | 18 + .claude/mcp-server.R | 12 + .claude/rules/git-workflow.md | 205 ++++++++ .claude/rules/jasp-containers-and-errors.md | 146 ++++++ .claude/rules/jasp-dependency-management.md | 136 ++++++ .claude/rules/jasp-module-architecture.md | 284 ++++++++++++ .claude/rules/jasp-output-structure.md | 197 ++++++++ .claude/rules/jasp-plots.md | 136 ++++++ .claude/rules/jasp-state-management.md | 257 +++++++++++ .claude/rules/jasp-tables.md | 202 ++++++++ .claude/rules/qml-instructions.md | 179 +++++++ .claude/rules/r-instructions.md | 121 +++++ .claude/rules/testing-instructions.md | 179 +++++++ .claude/rules/translation-instructions.md | 256 ++++++++++ .claude/session_startup.R | 50 ++ .claude/skills/fix-debug-analysis.md | 427 +++++++++++++++++ .codex/README.md | 85 ++++ .codex/config.toml | 19 + .codex/rules/default.rules | 97 ++++ .codex/rules/git-workflow.md | 198 ++++++++ .codex/rules/jasp-containers-and-errors.md | 141 ++++++ .codex/rules/jasp-dependency-management.md | 131 ++++++ .codex/rules/jasp-module-architecture.md | 278 +++++++++++ .codex/rules/jasp-output-structure.md | 191 ++++++++ .codex/rules/jasp-plots.md | 131 ++++++ .codex/rules/jasp-state-management.md | 252 ++++++++++ .codex/rules/jasp-tables.md | 197 ++++++++ .codex/rules/qml-instructions.md | 174 +++++++ .codex/rules/r-instructions.md | 116 +++++ .codex/rules/testing-instructions.md | 174 +++++++ .codex/rules/translation-instructions.md | 249 ++++++++++ .github/copilot-instructions.md | 262 +++++++++++ .github/instructions/R.instructions.md | 189 ++++++++ .../fix-debug-analysis.instructions.md | 432 +++++++++++++++++ .../instructions/git-workflow.instructions.md | 205 ++++++++ .github/instructions/inst.qml.instructions.md | 177 +++++++ ...jasp-containers-and-errors.instructions.md | 146 ++++++ ...jasp-dependency-management.instructions.md | 136 ++++++ .../jasp-module-architecture.instructions.md | 272 +++++++++++ .../jasp-output-structure.instructions.md | 196 ++++++++ .../instructions/jasp-plots.instructions.md | 136 ++++++ .../jasp-state-management.instructions.md | 257 +++++++++++ .../instructions/jasp-tables.instructions.md | 202 ++++++++ .github/instructions/testing.instructions.md | 164 +++++++ .../instructions/translation.instructions.md | 254 ++++++++++ .gitignore | 5 + AGENTS.md | 286 ++++++++++++ 51 files changed, 9398 insertions(+) create mode 100644 .agents/skills/fix-debug-analysis/SKILL.md create mode 100644 .claude/CLAUDE.md create mode 100644 .claude/README.md create mode 100644 .claude/hooks/block-test-edits.js create mode 100644 .claude/mcp-server.R create mode 100644 .claude/rules/git-workflow.md create mode 100644 .claude/rules/jasp-containers-and-errors.md create mode 100644 .claude/rules/jasp-dependency-management.md create mode 100644 .claude/rules/jasp-module-architecture.md create mode 100644 .claude/rules/jasp-output-structure.md create mode 100644 .claude/rules/jasp-plots.md create mode 100644 .claude/rules/jasp-state-management.md create mode 100644 .claude/rules/jasp-tables.md create mode 100644 .claude/rules/qml-instructions.md create mode 100644 .claude/rules/r-instructions.md create mode 100644 .claude/rules/testing-instructions.md create mode 100644 .claude/rules/translation-instructions.md create mode 100644 .claude/session_startup.R create mode 100644 .claude/skills/fix-debug-analysis.md create mode 100644 .codex/README.md create mode 100644 .codex/config.toml create mode 100644 .codex/rules/default.rules create mode 100644 .codex/rules/git-workflow.md create mode 100644 .codex/rules/jasp-containers-and-errors.md create mode 100644 .codex/rules/jasp-dependency-management.md create mode 100644 .codex/rules/jasp-module-architecture.md create mode 100644 .codex/rules/jasp-output-structure.md create mode 100644 .codex/rules/jasp-plots.md create mode 100644 .codex/rules/jasp-state-management.md create mode 100644 .codex/rules/jasp-tables.md create mode 100644 .codex/rules/qml-instructions.md create mode 100644 .codex/rules/r-instructions.md create mode 100644 .codex/rules/testing-instructions.md create mode 100644 .codex/rules/translation-instructions.md create mode 100644 .github/copilot-instructions.md create mode 100644 .github/instructions/R.instructions.md create mode 100644 .github/instructions/fix-debug-analysis.instructions.md create mode 100644 .github/instructions/git-workflow.instructions.md create mode 100644 .github/instructions/inst.qml.instructions.md create mode 100644 .github/instructions/jasp-containers-and-errors.instructions.md create mode 100644 .github/instructions/jasp-dependency-management.instructions.md create mode 100644 .github/instructions/jasp-module-architecture.instructions.md create mode 100644 .github/instructions/jasp-output-structure.instructions.md create mode 100644 .github/instructions/jasp-plots.instructions.md create mode 100644 .github/instructions/jasp-state-management.instructions.md create mode 100644 .github/instructions/jasp-tables.instructions.md create mode 100644 .github/instructions/testing.instructions.md create mode 100644 .github/instructions/translation.instructions.md create mode 100644 AGENTS.md diff --git a/.Rbuildignore b/.Rbuildignore index 7d392e5..a416287 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -3,3 +3,8 @@ ^.*\.Rproj$ ^\.Rproj\.user$ ^\.travis\.yml$ +^\.claude$ +^\.codex$ +^\.agents$ +^\.mcp\.json$ +^AGENTS\.md$ diff --git a/.agents/skills/fix-debug-analysis/SKILL.md b/.agents/skills/fix-debug-analysis/SKILL.md new file mode 100644 index 0000000..62d22a2 --- /dev/null +++ b/.agents/skills/fix-debug-analysis/SKILL.md @@ -0,0 +1,436 @@ +--- +name: fix-debug-analysis +description: > + Guide for debugging JASP analysis functions via code inspection and saveRDS + state capture. Use when fixing bugs, troubleshooting errors, or debugging R + analysis functions in JASP modules through MCP sessions. Also use when a user + provides a .jasp file to reproduce an issue. +--- + +# Fix & Debug JASP Analysis (MCP Session) + +Quick reference for debugging JASP analysis functions through MCP sessions. + +**Note**: `browser()` and `recover()` require interactive R console and **do not work** through MCP's `btw_tool_run_r`. + +--- + +## 1) Debugging Approaches + +There are two approaches, in order of preference: + +### Approach A: Code Inspection (try first) + +Many bugs — especially logic errors, missing branches, wrong conditions — are solvable by reading the code and tracing the control flow. This is faster and doesn't require instrumenting code. + +1. **Reproduce**: Bootstrap a `runAnalysis()` call (Step 0) and confirm the issue +2. **Read**: Trace the code path from the entry-point function through the relevant helpers +3. **Identify**: Look for logic errors — wrong conditions, missing option checks, incorrect branching +4. **Fix**: Edit the source, hot-reload, and verify + +**Use this when**: Output is missing, wrong options are checked, a feature works in one analysis type but not another, UI options don't match R-side logic. + +### Approach B: saveRDS State Capture (escalation) + +When the bug depends on runtime values that can't be deduced from code reading alone. + +1. **Instrument**: Add saveRDS() before the error location +2. **Capture**: Hot-reload and run analysis, copy debug path from console +3. **Inspect**: Load saved state and examine values via MCP +4. **Fix**: Develop and test fix using captured state +5. **Verify**: Remove debug code, hot-reload, confirm fix works + +**Use this when**: Error depends on specific data values, unexpected NULL/type, dimension mismatches, or the code path is too complex to trace by reading. + +--- + +## 2) Reproducing the Issue + +### Step 0: Bootstrap a Reproducible Analysis Run + +Before debugging, you need a working `runAnalysis()` call that reproduces the error. Choose the first applicable source: + +#### Option A: User provides a .jasp file + +```r +jaspFile <- "path/to/file.jasp" +opts <- jaspTools::analysisOptions(jaspFile) +dataset <- jaspTools::extractDatasetFromJASPFile(jaspFile) +encoded <- jaspTools:::encodeOptionsAndDataset(opts, dataset) +set.seed(1) +results <- jaspTools::runAnalysis("AnalysisName", encoded$dataset, encoded$options, encodedDataset = TRUE, view = FALSE) +``` + +If the .jasp file contains multiple analyses, `analysisOptions()` returns a list — index with `[[1]]`, `[[2]]`, etc. Pick the analysis that matches the error context. + +#### Option B: Extract from existing unit tests (most common fallback) + +When no .jasp file is provided, **search test files first**. Test files contain pre-configured options and dataset references that are known to produce complete output. + +1. **Find the test file** for the analysis in `tests/testthat/`: + ``` + grep -r "AnalysisName" tests/testthat/ + ``` + +2. **Determine the input pattern** used in the test. Tests use one of two patterns: + + **Pattern 1 — .jasp example file** (look for `analysisOptions(jaspFile)` or `extractDatasetFromJASPFile`): + ```r + # Copy the loading code from the test, adjusting the path for non-test context + jaspFile <- file.path("examples", "Example Name.jasp") + opts <- jaspTools::analysisOptions(jaspFile)[[1]] # note: may need [[1]] for multi-analysis files + dataset <- jaspTools::extractDatasetFromJASPFile(jaspFile) + encoded <- jaspTools:::encodeOptionsAndDataset(opts, dataset) + set.seed(1) + results <- jaspTools::runAnalysis("AnalysisName", encoded$dataset, encoded$options, encodedDataset = TRUE, view = FALSE) + ``` + + **Pattern 2 — inline options** (look for `analysisOptions("AnalysisName")` with explicit option assignments): + ```r + # Copy the options setup from the test verbatim + options <- jaspTools::analysisOptions("AnalysisName") + options$dependent <- "contNormal" # copy from test + options$group <- "contBinom" # copy from test + # ... copy ALL option assignments from the test ... + set.seed(1) + results <- jaspTools::runAnalysis("AnalysisName", "debug.csv", options, view = FALSE) + ``` + +3. **Modify options** to match the bug-triggering scenario (e.g., enable/disable specific checkboxes). + +4. **Verify reproduction**: Check that the issue is reproduced — this could be a `"fatalError"` status, an error message in a specific output element, incorrect values, missing output, etc., depending on what the user reported. + +#### Option C: Build options from scratch (last resort) + +Only when no tests or examples exist: + +```r +options <- jaspTools::analysisOptions("AnalysisName") +# Set required inputs — check .robttCheckReady() or equivalent readiness function +# to discover which options must be non-empty +options$dependent <- "contNormal" +options$group <- "contBinom" +set.seed(1) +results <- jaspTools::runAnalysis("AnalysisName", "debug.csv", options, view = FALSE) +``` + +**Tip**: `jaspTools::analysisOptions("AnalysisName")` returns all options with their QML defaults. Inspect it with `str(options)` to understand available options and their types. + +--- + +## 3) saveRDS Workflow (Approach B) + +Use these steps when code inspection alone is insufficient and you need to examine runtime values. + +### Step 1: Identify Error Location + +From the error message and stack trace, locate the function and approximate line where the error occurs. + +**Example**: Stack trace shows `.buildTable()` → `table$addFootnote()` → error + +### Step 2: Instrument Code + +Add saveRDS() just **before** the line that's failing: + +```r +.buildTable <- function(jaspResults, options) { + # ... existing code ... + + someVariable <- computeSomething(data, options) + + # DEBUG: REMOVE - save state before error + debug_dir <- tempdir() + saveRDS(list( + someVariable = someVariable, + relatedData = relatedData, + fit = fit, + options = options + # Include ALL relevant variables + ), file.path(debug_dir, "debug_state.rds")) + message("DEBUG: Saved to ", file.path(debug_dir, "debug_state.rds")) + + # The line that's failing + processData(someVariable) +} +``` + +**Critical rules**: +- Always use marker comment `# DEBUG: REMOVE` +- **Never save `jaspResults`** (crashes R) +- Save to `tempdir()` (auto-cleanup) +- Include `message()` to print path to console +- Save ALL variables that might be relevant + +### Step 3: Hot-Reload and Capture + +```r +# Via btw_tool_run_r in MCP +devtools::load_all() + +# Re-run the analysis (use same code that triggered original error) +set.seed(1) +results <- jaspTools::runAnalysis("AnalysisName", encoded$dataset, encoded$options, encodedDataset = TRUE, view = FALSE) + +# Console output will show: +# DEBUG: Saved to C:/Users/.../Temp/RtmpXXX/debug_state.rds +``` + +Copy the debug path from the console output. + +### Step 4: Inspect Captured State + +```r +# Via btw_tool_run_r in MCP +debug_path <- "C:/Users/.../Temp/RtmpXXX/debug_state.rds" +debug_data <- readRDS(debug_path) + +# Examine structure +str(debug_data) + +# Inspect specific variables +print(debug_data$someVariable) +sapply(debug_data$someVariable, class) +any(sapply(debug_data$someVariable, is.null)) + +# Check attributes +for (i in seq_along(debug_data$relatedData)) { + cat("Item", i, "attribute:", attr(debug_data$relatedData[[i]], "someAttr"), "\n") +} +``` + +**Goal**: Identify the exact values causing the error. + +### Step 5: Develop Fix + +Based on inspection, develop fix logic using the saved objects: + +```r +# Via btw_tool_run_r in MCP +# Test the fix logic interactively using saved state + +# Example: Filter out invalid values +someVariable_clean <- Filter(function(x) !is.null(x) && is.finite(x), debug_data$someVariable) +print(someVariable_clean) # Verify it works + +# Try the fix +for (i in seq_along(someVariable_clean)) { + cat("Would process item:", someVariable_clean[[i]], "\n") +} +``` + +Once fix logic works, implement it in the source file. + +### Step 6: Clean Up and Verify + +1. Apply fix to source file +2. **Remove all debug code** (saveRDS(), message(), and "# DEBUG: REMOVE" markers) +3. Hot-reload and verify: + +```r +# Via btw_tool_run_r in MCP +devtools::load_all() + +set.seed(1) +results <- jaspTools::runAnalysis("AnalysisName", encoded$dataset, encoded$options, encodedDataset = TRUE, view = FALSE) + +# Check overall status +cat("Status:", results$status, "\n") +``` + +4. **Verify the specific issue is resolved** — don't just check `results$status`: + - If the bug was missing output: confirm the output element now exists in `results$results` + - If the bug was wrong values: check the specific table/cell values + - If the bug was an error in a subcomponent: navigate to that component and verify no error + - If the bug was a crash: confirm status is `"complete"` + +5. Search for any remaining debug code before committing: + +```bash +grep -r "DEBUG: REMOVE" R/ +grep -r "saveRDS.*tempdir" R/ +``` + +--- + +## 4) What to Save + +| Location | Objects to save | DON'T save | +|----------|----------------|------------| +| **Model fitting** | `dataset`, `options`, function args, intermediate values | `jaspResults`, `...` (ellipsis args) | +| **Row building** | `fit`, `attr(fit, "group")`, computed rows, `options` | Parent containers, environments | +| **Table assembly** | `rows` list, intermediate data.frames | Full fit objects if not needed | +| **Error handling** | Error object, variables being processed when error occurred | Large intermediate objects | + +**Golden rule**: When unsure, save it. Missing a variable means re-running the entire capture process. + +--- + +## 5) Real-World Example + +**Error**: `jaspTable$addFootnote expects 'message' to be a string!` + +**Workflow**: + +1. **Loaded .jasp file and reproduced error**: + ```r + jaspFile <- "path/to/file.jasp" + opts <- jaspTools::analysisOptions(jaspFile) + dataset <- jaspTools::extractDatasetFromJASPFile(jaspFile) + encoded <- jaspTools:::encodeOptionsAndDataset(opts, dataset) + set.seed(1) + results <- jaspTools::runAnalysis("AnalysisName", encoded$dataset, encoded$options, encodedDataset = TRUE, view = FALSE) + # → Status: fatalError + ``` + +2. **Identified error location**: Stack trace → `.buildTable()` at specific line + +3. **Instrumented code**: + ```r + footnotes <- unique(lapply(dataList, attr, which = "footnote")) + + # DEBUG: REMOVE + saveRDS(list( + footnotes = footnotes, + dataList = dataList + ), file.path(tempdir(), "footnote_debug.rds")) + message("DEBUG: Saved to ", file.path(tempdir(), "footnote_debug.rds")) + + for (i in seq_along(footnotes)) + table$addFootnote(footnotes[[i]]) + ``` + +4. **Captured state**: + ```r + devtools::load_all() + results <- jaspTools::runAnalysis(..., view = FALSE) + # Console: DEBUG: Saved to C:/Users/.../RtmpXXX/footnote_debug.rds + ``` + +5. **Inspected**: + ```r + debug_data <- readRDS("C:/Users/.../RtmpXXX/footnote_debug.rds") + str(debug_data$footnotes) + # List of 2 + # $ : chr "Some footnote text..." + # $ : NULL ← THE PROBLEM + ``` + +6. **Root cause**: `unique()` preserves NULL values → loop called `addFootnote(NULL)` → error + +7. **Implemented fix**: + ```r + footnotes <- unique(lapply(dataList, attr, which = "footnote")) + footnotes <- Filter(Negate(is.null), footnotes) # Filter NULLs + for (i in seq_along(footnotes)) + table$addFootnote(footnotes[[i]]) + ``` + +8. **Verified**: + ```r + devtools::load_all() + results <- jaspTools::runAnalysis(..., view = FALSE) + # → Status: complete ✓ + ``` + +**Time**: ~5 minutes from error to verified fix. + +--- + +## 6) Advanced Techniques + +### Conditional Saving + +For errors in specific iterations/groups: + +```r +# Only save when condition is met +for (i in seq_along(items)) { + if (i == 47) { # Error only in iteration 47 + saveRDS(list(item = items[[i]], i = i), file.path(tempdir(), "debug_iter47.rds")) + message("DEBUG: Saved iteration 47") + } + result <- process(items[[i]]) +} +``` + +### Multiple Checkpoints + +Narrow down error location by saving at multiple points: + +```r +# Checkpoint 1 +saveRDS(list(step = "before_transform", data = data), + file.path(tempdir(), "checkpoint1.rds")) + +data_transformed <- transform(data) + +# Checkpoint 2 +saveRDS(list(step = "after_transform", data_transformed = data_transformed), + file.path(tempdir(), "checkpoint2.rds")) +``` + +### Save with Timestamp + +For multiple runs: + +```r +timestamp <- format(Sys.time(), "%Y%m%d_%H%M%S") +saveRDS(list(...), file.path(tempdir(), paste0("debug_", timestamp, ".rds"))) +``` + +--- + +## 7) Common Error Patterns + +### Pattern 1: Unexpected NULL + +**Symptom**: "argument is NULL" or "expects X to be a Y" + +**Debugging**: +```r +saveRDS(list(suspect_var = suspect_var, related_vars = list(...)), ...) +# Inspect: is.null(debug_data$suspect_var) +``` + +### Pattern 2: Wrong Type/Class + +**Symptom**: "cannot coerce X to Y" or "is not a valid type" + +**Debugging**: +```r +saveRDS(list(var = var, class = class(var), str = capture.output(str(var))), ...) +# Inspect: class(debug_data$var), attributes(debug_data$var) +``` + +### Pattern 3: Dimension Mismatch + +**Symptom**: "dims [product X] do not match length of object [Y]" + +**Debugging**: +```r +saveRDS(list(obj = obj, dims = dim(obj), length = length(obj)), ...) +# Inspect: dim(debug_data$obj), length(debug_data$obj) +``` + +### Pattern 4: Index Out of Bounds + +**Symptom**: "subscript out of bounds" or "undefined columns selected" + +**Debugging**: +```r +saveRDS(list(container = container, index = i, length = length(container)), ...) +# Inspect: i vs length(debug_data$container), names(debug_data$container) +``` + +--- + +## 8) Safety Checklist + +Before committing code: + +- [ ] All `# DEBUG: REMOVE` markers removed +- [ ] All `saveRDS()` calls removed +- [ ] All debug `message()` calls removed +- [ ] Verified with: `grep -r "DEBUG: REMOVE" R/` +- [ ] Verified with: `grep -r "saveRDS.*tempdir" R/` +- [ ] Hot-reloaded and tested: analysis completes successfully diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 100644 index 0000000..b50c88a --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1,275 @@ +# JASP Module + +ALWAYS follow these instructions first and fallback to additional search and context gathering ONLY if the information in these instructions is incomplete or found to be in error. + +This is a JASP module. It contains QML user-facing interfaces and R backend computations. + +In all interactions and commit messages, be extremely concise and sacrifice grammar for the sake of concision. + +## Detailed Instructions + +For comprehensive guidance on specific topics, see: + +- **[Module Architecture](.claude/rules/jasp-module-architecture.md)** - **Start here.** QML-Desktop-R reactive loop, jaspResults persistence, options mapping, data flow +- **[Dependency Management](.claude/rules/jasp-dependency-management.md)** - $dependOn mechanics, inheritance, vectors, per-value deps, sentinel pattern +- **[State Management](.claude/rules/jasp-state-management.md)** - createJaspState caching, model fit patterns, metadata state, dynamic containers +- **[R Backend Development](.claude/rules/r-instructions.md)** - R function structure, validation, style conventions +- **[Tables](.claude/rules/jasp-tables.md)** - Table lifecycle, columns, rows, footnotes, error display +- **[Plots](.claude/rules/jasp-plots.md)** - Plot lifecycle, composite plots, subgroup/facet patterns +- **[Containers & Errors](.claude/rules/jasp-containers-and-errors.md)** - Container patterns, HTML output, error handling +- **[QML Interface Development](.claude/rules/qml-instructions.md)** - QML controls, validation, bindings, and UI patterns +- **[Testing & Test Writing](.claude/rules/testing-instructions.md)** - Test framework, snapshots, and test workflow +- **[Translation (i18n)](.claude/rules/translation-instructions.md)** - gettext/gettextf/qsTr usage, formatting, plurals +- **[Output Structure](.claude/rules/jasp-output-structure.md)** - Reading/testing serialized output (containers, tables, plots, state) +- **[Git Workflow](.claude/rules/git-workflow.md)** - Commit message style, branch strategy, PR guidelines, git safety rules + +## R Session via MCP + +This project uses the `btw` MCP server (`.claude/mcp-server.R`) to provide a persistent R session via `btw_tool_run_r`. The MCP server config (`.mcp.json`) is module-specific and NOT committed to git. + +**Session handoff:** The user sets up their R session (RStudio/Positron/radian), runs `btw::btw_mcp_session()`, and hands it over. Connect via `list_r_sessions` / `select_r_session`. All `btw_tool_run_r` calls then execute in the user's session with full access to loaded packages and objects. The following R packages are required for the mcp server: `btw`, `mcptools`. + +### Available MCP Tools + +These are MCP tools — invoke them directly as tool calls, not as R functions or shell commands: + +| Tool | Use for | +|------|---------| +| `list_r_sessions` | Discover available R sessions (call first) | +| `select_r_session` | Connect to a session from the list | +| `btw_tool_run_r` | Execute R code in persistent session (variables persist between calls) | +| `btw_tool_docs_help_page` | Look up R function documentation | +| `btw_tool_docs_package_news` | Check package changelogs | +| `btw_tool_docs_available_vignettes` | Find package vignettes | +| `btw_tool_env_describe_environment` | Inspect objects in the R session | +| `btw_tool_env_describe_data_frame` | Inspect data frame structure | +| `btw_tool_search_packages` | Search CRAN for packages | +| `btw_tool_session_platform_info` | Check R version and platform | +| `btw_tool_session_check_package_installed` | Verify package availability | + +**Use native tools** (Read, Edit, Write, Glob, Grep, Bash) for file editing, git operations, and file search -- they are faster than MCP equivalents. + +## Working Effectively + +### Session Setup (done by user) + +At the start of a session, check for a connected R session via `list_r_sessions`. If none is available, **prompt the user** to run in their interactive R console: + +```r +source(".claude/session_startup.R") +``` + +This restores dependencies, installs the module, configures jaspTools, and registers the session. Then connect via `list_r_sessions` / `select_r_session`. + +### Hot-Reload After Code Changes + +- **R code only changed:** `devtools::load_all()` via `btw_tool_run_r` +- **QML, dependencies, or imports changed:** `renv::install(".", prompt = FALSE)` + +### Running Tests + +Run via `btw_tool_run_r` in the persistent session: + +**Agent-optimized** (preferred -- compact output, returns queryable result object): + +```r +# Full test suite -- returns rich S3 result object +x <- agentTestAll() + +# Specific analysis tests +x <- agentTestAnalysis("AnalysisName") +``` + +These return a `jaspAgentTestResults` object. Console output is a compact one-line summary: +``` +== Test Results == FAIL: 0 | WARN: 0 | SKIP: 2 | PASS: 72 | Time: 3.6s +``` + +Query the result object directly: +```r +x$status # 0 = all passed, 1 = failures +x$summary # list(fail, warn, skip, pass, time) +x$failures # data.frame: module | file | test | message +x$warnings # data.frame: module | file | test | message +x$skips # data.frame: module | file | test | reason +x$tests # data.frame: all tests with module | file | context | test | passed | failed | ... +x$errorModules # named character vector of module-level errors +x$logFile # path to detailed JSON log (with backtraces) +``` + +**Human-oriented** (verbose output, for interactive use): +```r +testAll() +testAnalysis("AnalysisName") +``` + +**Rules:** +- Tests take 300+ seconds to complete -- **NEVER CANCEL** +- Run `agentTestAll()` at session start to verify baseline, and after all fixes +- Use `agentTestAnalysis("Name")` for quick iteration on specific analyses +- Analysis names are PascalCase exports from NAMESPACE +- Some tests skip on certain platforms (e.g., Windows) -- expected +- Some stderr noise (ggplot messages, tryCatch errors) may leak through -- expected and minor +- **MCP timeout:** If `btw_tool_run_r` times out on `agentTestAll()`, do NOT retry -- use the Bash fallback in [testing-instructions.md](.claude/rules/testing-instructions.md) + +**See [testing-instructions.md](.claude/rules/testing-instructions.md) for detailed test writing guidelines, snapshots, and workflows.** + +### Running a Specific Analysis + +**With built-in debug dataset:** +```r +options <- jaspTools::analysisOptions("AnalysisName") +options$someOption <- value +set.seed(1) +results <- jaspTools::runAnalysis("AnalysisName", "debug.csv", options, view = FALSE) +``` + +**From a .jasp example file:** +```r +jaspFile <- file.path("examples", "Example Name.jasp") +opts <- jaspTools::analysisOptions(jaspFile) +dataset <- jaspTools::extractDatasetFromJASPFile(jaspFile) +encoded <- jaspTools:::encodeOptionsAndDataset(opts, dataset) +set.seed(1) +results <- jaspTools::runAnalysis("AnalysisName", encoded$dataset, encoded$options, encodedDataset = TRUE, view = FALSE) +``` + +The encoding step is required because JASP internally encodes variable names and options to resolve ambiguities (e.g., same variable used with different types). + +**From a user-provided .jasp file:** Use the same pattern above. This is the primary way to reproduce bugs reported by users. + +**NEVER instantiate jaspResults C++ objects directly** (e.g., `jaspResultsClass$new()`, `create_cpp_jaspResults()`, `jaspBase:::initJaspResults()`). These require JASP Desktop C++ initialization unavailable in headless R sessions. They crash with `Rcpp::not_initialized` or `Expecting an external pointer`. Always use `jaspTools::runAnalysis()` or `agentTestAll()` which handle initialization internally. + +### Inspecting Results + +Always set `view = FALSE` when running `runAnalysis()` manually. This avoids HTML generation; inspect the returned R object instead. + +After `runAnalysis()`, check: +- `results$status` -- `"complete"` or `"fatalError"` +- `results$results` -- nested list of output containers, tables, plots +- `results$results$errorMessage` -- if status is fatalError + +### Finding Analysis Names + +1. Check roxygen documentation in R files (if available) +2. Parse `NAMESPACE` for `export()` directives + +### Test Snapshots + +- Snapshots stored in `tests/testthat/_snaps/` +- **NEVER automatically accept snapshot changes** -- always notify user for manual inspection +- When a snapshot is newly created, inform the user + +### Repository Structure +``` +/ +├── R/ # Backend R analysis functions +├── inst/ +│ ├── qml/ # QML interface definitions +│ ├── Descriptions/ # Analysis descriptions (Description.qml) +│ ├── help/ # Markdown help files +│ └── Upgrades.qml # Version upgrade mappings +├── examples/ # Example .jasp files for testing +├── tests/testthat/ # Unit tests using jaspTools +├── .claude/ # Claude Code instructions and MCP server +│ ├── CLAUDE.md # This file +│ ├── mcp-server.R # MCP server startup script +│ └── rules/ # Path-specific rules +├── .github/workflows/ # CI/CD automation +├── DESCRIPTION # R package metadata +├── NAMESPACE # Exported analysis names +└── renv.lock # R dependency lockfile +``` + +### Key Files to Check After Changes +- Always check corresponding test file in `tests/testthat/` when modifying R functions +- For released analyses, update `inst/Upgrades.qml` when renaming QML options to maintain backward compatibility. For unreleased analyses, keep only the current QML/R names. + +## Development Rules + +### Dependencies +- Avoid new dependencies -- re-implement simple functions instead of importing a whole package +- If a new dependency is truly needed, add it to DESCRIPTION and update renv.lock + +### QML Interface Rules +- QML interfaces in `inst/qml/` define user-facing options passed to R functions +- Each analysis links: `inst/Description.qml/` -> `inst/qml/` -> `R/` functions +- QML elements use `name` (camelCase internal) and `title`/`label` (user-facing) +- QML `name` values are the exact R option API. When changing option names, update R reads and dependency vectors to the current names; do not keep old aliases for unreleased analyses. +- Keep maintainable imported QML components when they reduce duplication/clutter. Do not inline or flatten QML solely because `jaspTools::analysisOptions()` cannot see imported components or dynamic bindings. +- Verify QML/R option contracts with source-aware checks across the main QML file and imported components; when tests need defaults tooling cannot derive, pass explicit GUI-equivalent options instead of adding R defaults. +- Preserve dynamic `DropDown.values` when they are the clearer GUI. Use static values plus `enabledOptions` only when that is the intended UX, not as a tooling workaround. +- Document QML elements using `info` property for help generation +- Use existing QML files as examples for structure and style +- Add default values to unit tests when adding new QML options + +**See [qml-instructions.md](.claude/rules/qml-instructions.md) for comprehensive QML controls reference, validation patterns, and UI conventions.** + +### R Backend Rules +- R functions in `R/` directory called by analyses in `inst/Descriptions/` +- Use camelCase for all function and variable names +- NEVER use `library()` or `require()` - use `package::function()` syntax +- Access `options` list via `options[["name"]]` notation to avoid partial matching +- Treat GUI options as a strict contract: do not add R-side normalization, alias maps, compatibility layers, or backup defaults for missing QML options in unreleased work. Missing/disconnected options should fail so the QML/R mismatch is fixed. +- For checkbox options, use `if (options[["flag"]])`, not `isTRUE(options[["flag"]])`; `isTRUE()` masks missing options. +- Follow CRAN guidelines for code structure and documentation + +**See [r-instructions.md](.claude/rules/r-instructions.md) for complete R function structure, jaspResults API, output components (tables/plots/containers/state), and coding conventions.** + +### Input Validation and Error Handling +- **TARGETED VALIDATION ONLY**: Since `options` are validated in the GUI, R functions should NOT check user input validity except for specific cases +- **VALIDATE ONLY**: `dataset` object (data.frame from GUI), `TextField` options, and `FormulaField` options (arbitrary text input) +- Use `gettext()` and `gettextf()` for all user-visible messages (internationalization) +- For `dataset` validation, check: missing values, infinity, negative values, insufficient observations, factor levels, variance +- Example: `.hasErrors(dataset, type = c('observations', 'variance', 'infinity'), all.target = options$variables, observations.amount = '< 3', exitAnalysisIfErrors = TRUE)` +- Validate dataset assumptions automatically when required for analysis validity +- Use footnotes for assumption violations that affect specific cells/values +- Place critical errors that invalidate entire analysis over the results table + +### Error Message Guidelines +- Write clear, actionable error messages that prevent user confusion +- Use `gettextf()` with placeholders for dynamic content: `gettextf("Number of factor levels is %1$s in %2$s", levels, variable)` +- For multiple arguments, use `%1$s`, `%2$s` format for translator clarity +- Use `ngettext()` for singular/plural forms +- Never mark empty strings for translation +- Use UTF-8 encoding for non-ASCII characters: `\u03B2` for beta +- Double `%` characters in format strings: `gettextf("%s%% CI for Mean")` + +**See [translation-instructions.md](.claude/rules/translation-instructions.md) for comprehensive i18n guidelines including QML qsTr(), R gettext/gettextf/ngettext, formatting rules, and Weblate workflow.** + +## CI/CD Pipeline +- GitHub Actions in `.github/workflows/unittests.yml` runs on every push +- Triggers on changes to R, test, or package files +- Uses jasp-stats/jasp-actions reusable workflow + +## Git Workflow + +- **ALWAYS work on feature branches** -- never commit directly to `master` +- **NEVER push/create PRs/merge without explicit human approval** +- Commit locally freely, but wait for approval before pushing to remote + +## Common Tasks + +### Adding New Analysis + +1. Create R function in `R/` directory following camelCase naming +2. Add QML interface in `inst/qml/` +3. Define analysis in `inst/Description.qml` +4. Add unit tests in `tests/testthat/` +5. Run `agentTestAll()` to validate (300+ seconds, NEVER CANCEL) + +### Modifying Existing Analysis + +1. Update R function maintaining existing interface +2. Update QML if adding/changing options +3. Update unit tests and expected results +4. Add upgrade mapping to `inst/Upgrades.qml` if renaming options for a released analysis +5. Run tests: `agentTestAll()` (NEVER CANCEL, 300+ seconds) + +### Detailed Development Process +- **Step 1**: Create main analysis function with `jaspResults`, `dataset`, `options` arguments +- **Step 2**: **CRITICAL** - Use `.quitAnalysis()` for `dataset`, `TextField`, `FormulaField` validation only +- **Step 3**: Create output tables/plots with proper dependencies, citations, column specs +- 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 diff --git a/.claude/README.md b/.claude/README.md new file mode 100644 index 0000000..b059d11 --- /dev/null +++ b/.claude/README.md @@ -0,0 +1,125 @@ +# Claude Code Instructions + +This directory contains project-specific instructions for Claude Code, Anthropic's CLI tool. + +## Purpose + +These files are automatically loaded when Claude Code starts, providing context about: + +- JASP module structure and conventions +- Development workflows and best practices +- Testing requirements +- Translation guidelines + +## Structure + +``` +.claude/ +├── CLAUDE.md # Main project instructions (always loaded) +├── README.md # This file +├── mcp-server.R # MCP server startup script (R session tools) +├── settings.local.json # Local Claude Code settings (not committed) +└── rules/ # Path-specific rules + ├── r-instructions.md # R backend guidelines (**/R/*.R) + ├── qml-instructions.md # QML interface guidelines (**/inst/qml/*.qml) + ├── testing-instructions.md # Test framework guidelines (**/tests/testthat/*.R) + ├── git-workflow.md # Git and commit conventions + └── translation-instructions.md # i18n/l10n guidelines +``` + +## MCP Server Setup + +The `.claude/mcp-server.R` script configures the `btw` MCP server for JASP module development. It: + +1. Enables `btw_tool_run_r` for R code execution in a persistent session +2. Fixes `cli.spinner` option for testthat compatibility in the evaluate context +3. Exposes btw tool groups: docs, env, run, search, session + +The user sets up their R session, then registers it via `btw::btw_mcp_session()`. Claude connects with `list_r_sessions` / `select_r_session` and executes R code in the user's session. + +### Configuration + +The MCP server is configured via `.mcp.json` in the module root (NOT committed to git). To set up: + +```json +{ + "mcpServers": { + "r-mcptools": { + "type": "stdio", + "command": "Rscript", + "args": ["-e", "source('.claude/mcp-server.R')"] + } + } +} +``` + +Or via CLI: `claude mcp add r-mcptools -- Rscript -e "source('.claude/mcp-server.R')"` + +### Connecting an Interactive R Session + +To route MCP tool calls to your interactive R session (RStudio/Positron/radian): + +```r +btw::btw_mcp_session() +``` + +This gives Claude Code access to your loaded objects and environment. + +## How It Works + +**Automatic Loading:** + +- `CLAUDE.md` is automatically loaded in every Claude Code session +- Files in `rules/` are loaded based on their `paths:` frontmatter +- Path-specific rules apply only when working on matching files + +**Path Scoping:** +Rules use YAML frontmatter to scope to specific files: + +```yaml +--- +paths: + - "**/R/*.R" +--- +``` + +## Copying to Other JASP Modules + +To use these instructions in another JASP module: + +1. Copy the `.claude/` directory to the target module +2. Create a `.mcp.json` in the module root (see Configuration above) +3. Adjust the `Rscript` command path if needed for your system +4. The `.mcp.json` file should be added to `.gitignore` (machine-specific paths) +5. The `.claude/mcp-server.R` script is portable and can be committed + +## Personal Preferences + +To add personal project-specific preferences that aren't shared with the team: + +1. Create `CLAUDE.local.md` in this directory +2. Add your personal preferences +3. File is already in `.gitignore` and won't be committed + +## Maintenance + +**When to update:** + +- Adding new development conventions +- Changing testing requirements +- Updating build/deployment processes +- Adding new repository-specific workflows + +**What to include:** + +- Information Claude can't infer from code +- Project-specific conventions that differ from defaults +- Critical commands and workflows +- Non-obvious patterns and gotchas + +**What to exclude:** + +- Standard language conventions +- Detailed API documentation (link to it instead) +- Frequently changing information +- Information easily discovered by reading code diff --git a/.claude/hooks/block-test-edits.js b/.claude/hooks/block-test-edits.js new file mode 100644 index 0000000..ccc3315 --- /dev/null +++ b/.claude/hooks/block-test-edits.js @@ -0,0 +1,18 @@ +// PreToolUse hook: blocks Edit/Write on files under tests/ +// Contract: read JSON from stdin, echo it to stdout, exit 2 to block +let d = ''; +process.stdin.on('data', c => d += c); +process.stdin.on('end', () => { + try { + const input = JSON.parse(d); + const filePath = input.tool_input?.file_path || ''; + if (/(^|[/\\])tests[/\\]/.test(filePath)) { + process.stderr.write( + '[Hook] BLOCKED: test files are human-owned. Fix source code instead.\n' + ); + console.log(d); + process.exit(2); + } + } catch {} + console.log(d); +}); diff --git a/.claude/mcp-server.R b/.claude/mcp-server.R new file mode 100644 index 0000000..275d7a0 --- /dev/null +++ b/.claude/mcp-server.R @@ -0,0 +1,12 @@ +# Custom MCP server for JASP modules +# Provides btw tools with JASP-specific fixes +options( + btw.run_r.enabled = TRUE, + # Fix cli::get_spinner() returning FALSE in evaluate context, + # which breaks testthat reporter initialization (which$frames error) + cli.spinner = "line" +) + +btw::btw_mcp_server( + tools = btw::btw_tools("docs", "env", "run", "search", "session") +) diff --git a/.claude/rules/git-workflow.md b/.claude/rules/git-workflow.md new file mode 100644 index 0000000..8f4327b --- /dev/null +++ b/.claude/rules/git-workflow.md @@ -0,0 +1,205 @@ +--- +paths: + - "**" +--- + +# Git Workflow Instructions + +## Commit Message Style + +**Be extremely concise. Sacrifice grammar for concision.** + +### Format: +``` +: + +[optional body if needed] + +Co-Authored-By: Claude Sonnet 4.5 +``` + +### Types: +- `feat:` - New feature or analysis +- `fix:` - Bug fix +- `refactor:` - Code restructuring without behavior change +- `test:` - Adding or updating tests +- `docs:` - Documentation only +- `i18n:` - Translation updates +- `chore:` - Maintenance tasks + +### Examples: +``` +feat: add equivalence bounds plot + +fix: correct CI calculation in paired t-test + +test: update snapshots for descriptives table + +refactor: extract common validation logic + +i18n: update translation files +``` + +## Commit Workflow + +### 0. Ensure on feature branch: +```bash +# Check current branch +git branch + +# If on master, create feature branch +git checkout -b feature/descriptive-name +``` + +### 1. Before committing: +```bash +# Run full test suite +Rscript -e "library(jaspTools); agentTestAll()" + +# Check git status +git status + +# Review changes +git diff +``` + +### 2. Stage specific files: +```bash +# Stage specific files (preferred) +git add R/equivalenceonesamplettest.R +git add tests/testthat/test-equivalenceonesamplettest.R + +# Avoid staging everything unless you're certain +# git add -A # Be careful with this +``` + +### 3. Commit locally with co-author: +```bash +git commit -m "$(cat <<'EOF' +feat: add descriptives table + +Co-Authored-By: Claude Sonnet 4.5 +EOF +)" +``` + +**Local commits are OK. Pushing to remote requires human approval.** + +## Pre-Commit Requirements + +Before every commit, ensure: +- ✅ All tests pass (`jaspTools::agentTestAll()`) +- ✅ No unintended files staged (.env, credentials, etc.) +- ✅ Commit message is concise and descriptive +- ✅ Changes are focused and related + +## Branch Strategy + +- **Main branch:** `master` +- **NEVER work directly on `master` branch** +- **ALWAYS create a feature branch for any changes:** + ```bash + git checkout -b feature/descriptive-name + ``` +- Branch naming conventions: + - `feature/description` - New features or analyses + - `fix/description` - Bug fixes + - `refactor/description` - Code restructuring + - `test/description` - Test updates + +## Pull Request Guidelines + +**CRITICAL: NEVER push to remote, create PRs, or merge without explicit human approval.** + +Human must review all local changes before they go online. + +When human approves creating a PR: +1. Ensure all tests pass locally first +2. Keep PR scope focused and small +3. Use concise PR title (same style as commits) +4. Summarize changes in bullet points +5. Note any breaking changes +6. Wait for human to review the PR description before posting + +## What NOT to Commit + +- ❌ `.Rhistory`, `.RData`, `.Rproj.user/` +- ❌ Test artifacts or temporary files +- ❌ Personal IDE settings +- ❌ Large data files +- ❌ Credentials or API keys +- ❌ `CLAUDE.local.md` (personal preferences) + +## CI/CD Integration + +- GitHub Actions runs tests on every push +- Workflow file: `.github/workflows/unittests.yml` +- Tests must pass for PR to be merged +- Translation workflows run on schedule + +## Git Safety + +- **NEVER** work directly on `master` branch - always use feature branches +- **NEVER** push to remote without explicit human approval +- **NEVER** create pull requests without explicit human approval +- **NEVER** merge changes without explicit human approval +- **NEVER** force push to any branch +- **NEVER** amend published commits +- **NEVER** skip hooks unless explicitly needed +- **NEVER** commit without running tests first + +**Human must approve all changes before they go online.** + +## Common Git Commands + +```bash +# Check current branch +git branch + +# Create and switch to feature branch +git checkout -b feature/description + +# Check status +git status + +# View changes +git diff +git diff --staged + +# Stage specific files +git add + +# Commit locally (OK to do without approval) +git commit -m "message" + +# View recent commits +git log --oneline -5 + +# View commit history with graph +git log --graph --oneline --all -10 + +# === REQUIRE HUMAN APPROVAL BEFORE RUNNING: === + +# Push to remote (WAIT FOR APPROVAL) +git push origin feature/description + +# Pull latest changes (usually safe, but confirm first) +git pull origin master +``` + +## Handling Test Failures + +If CI tests fail after human has pushed: +1. Check GitHub Actions output +2. Reproduce failure locally +3. Fix the issue +4. Run tests to confirm fix +5. Commit locally +6. Ask human for approval to push fix + +## Translation Commits + +Translation updates are handled automatically: +- Weblate integration updates translation files +- Automated commits from translation workflow +- Don't manually edit translation files unless necessary diff --git a/.claude/rules/jasp-containers-and-errors.md b/.claude/rules/jasp-containers-and-errors.md new file mode 100644 index 0000000..d7d345d --- /dev/null +++ b/.claude/rules/jasp-containers-and-errors.md @@ -0,0 +1,146 @@ +--- +paths: + - "**/R/*.R" +--- + +# JASP Containers, HTML Output & Error Handling + +Patterns for grouping output elements and handling errors in jaspResults. + +For tables see [jasp-tables.md](jasp-tables.md). +For plots see [jasp-plots.md](jasp-plots.md). +For state/caching see [jasp-state-management.md](jasp-state-management.md). + +--- + +## 1) Containers + +Containers group related output elements under a collapsible section. + +### Get-or-create pattern (reusable across multiple builder functions) + +```r +.myExtractContainer <- function(jaspResults) { + if (!is.null(jaspResults[["myContainer"]])) + return(jaspResults[["myContainer"]]) + + container <- createJaspContainer(gettext("My Section Title")) + container$dependOn(.myBaseDependencies) + container$position <- 1 + jaspResults[["myContainer"]] <- container + + return(container) +} +``` + +- Use a dedicated extractor when **multiple builder functions** write to the same container +- `$position` controls display order (lower = higher on page) +- `$dependOn()` on the container invalidates **all children** when base options change + +### Direct creation (when only one function writes to it) + +```r +if (is.null(jaspResults[["sectionContainer"]])) { + container <- createJaspContainer(gettext("Section Title")) + container$dependOn(c(.baseDependencies, "specificOption")) + container$position <- 4 + jaspResults[["sectionContainer"]] <- container +} +``` + +### Nested containers + +For deeply hierarchical output (e.g., per-variable tables): + +```r +outerContainer <- jaspResults[["outer"]] +innerContainer <- createJaspContainer(title = "Variable X") +innerContainer$position <- i +outerContainer[["variableX"]] <- innerContainer +# then add tables/plots to innerContainer +``` + +### Dynamic container management + +When the set of children depends on user-selected variables: + +```r +# Track existing vs selected variables via metadata state +existingVariables <- metaData[["existingVariables"]] +selectedVariables <- getSelectedVariables(options) + +# Remove deselected +for (v in setdiff(existingVariables, selectedVariables)) + container[[v]] <- NULL + +# Add new +for (v in setdiff(selectedVariables, existingVariables)) { + childContainer <- createJaspContainer(title = v) + container[[v]] <- childContainer + .buildChildTable(childContainer, fit, options, v) +} + +# Update metadata +metaDataState$object <- list(existingVariables = selectedVariables) +``` + +See [jasp-state-management.md](jasp-state-management.md) for the metadata state pattern that powers this. + +--- + +## 2) HTML Output + +For raw HTML content (e.g., displaying R code or formatted messages): + +```r +htmlOutput <- createJaspHtml(title = gettext("R Code")) +htmlOutput$dependOn(c(.baseDependencies, "showCode")) +htmlOutput$position <- 99 +htmlOutput$text <- "
myFunction(yi = ..., sei = ...)
" +jaspResults[["rCode"]] <- htmlOutput +``` + +--- + +## 3) Error Handling Patterns + +### Create-then-error + +Always **attach the element to jaspResults before checking errors**. This ensures the empty table (with error message) is displayed rather than nothing: + +```r +table <- createJaspTable(gettext("Title")) +container[["table"]] <- table # attach FIRST + +# THEN check for errors +if (someError) { + table$setError(errorMessage) + return() +} +``` + +### Graceful degradation with groups + +When some per-group fits fail but others succeed, show partial results with per-group error footnotes: + +```r +# Row builders return skeleton data.frames on error (labels only, NAs for numeric columns) +# Tables show partial results with error footnotes per failed group +for (i in which(sapply(fit, jaspBase::isTryError))) { + table$addFootnote( + gettextf("Group '%1$s' failed: %2$s", attr(fit[[i]], "group"), .cleanError(fit[[i]])), + symbol = gettext("Error:") + ) +} +``` + +### Total failure + +When the entire fit fails: + +```r +if (length(fit) == 1 && jaspBase::isTryError(fit[[1]])) { + table$setError(.cleanErrorMessage(fit[[1]])) + return() +} +``` diff --git a/.claude/rules/jasp-dependency-management.md b/.claude/rules/jasp-dependency-management.md new file mode 100644 index 0000000..0d5084d --- /dev/null +++ b/.claude/rules/jasp-dependency-management.md @@ -0,0 +1,136 @@ +--- +paths: + - "**/R/*.R" +--- + +# JASP Dependency Management ($dependOn) + +How `$dependOn()` controls caching and invalidation of output elements in jaspResults. + +For the reactive loop context see [jasp-module-architecture.md](jasp-module-architecture.md). + +Note that you cannot test this by running analysis via `runAnalysis(..., view = FALSE)` because you only generate one state at a time +(with no initial elements - ask the human maintainer to validate the dependencies manually if you suspect an issue!). + +--- + +## 1) What $dependOn Does + +When you write: +```r +table$dependOn(c("method", "ciLevel")) +``` + +You tell JASP Desktop: "If `options[["method"]]` or `options[["ciLevel"]]` changes, set this element to NULL before calling R." On the next R invocation, the builder's `if (!is.null(...))` guard sees NULL and recreates the element. + +Elements whose dependencies are NOT hit survive across invocations -- the builder returns early and the existing output stays on screen. + +--- + +## 2) Dependency Inheritance + +Container dependencies propagate to ALL children: + +```r +container$dependOn(c("dependentVariable", "method")) # base deps +table$dependOn(c("showCI")) # additional dep +container[["myTable"]] <- table +``` + +The table is invalidated if `dependentVariable`, `method`, OR `showCI` changes. Never repeat parent deps on children. + +This means you can put shared model-level dependencies on the container and only add output-specific deps to individual tables/plots. + +--- + +## 3) Dependency Vectors as Constants + +Define at file top for reuse across builders: +```r +.baseDeps <- c("dependentVariable", "covariates", "method", "ciLevel") +.plotDeps <- c("plotColor", "plotSize", "plotTheme") +``` + +Use in builders: +```r +container$dependOn(.baseDeps) # container holds base deps +table$dependOn(c("showResiduals")) # child adds specific dep +plot$dependOn(c(.baseDeps, .plotDeps)) # or combine for standalone elements +``` + +Keep dependency vectors comprehensive -- missing a dependency means stale output when that option changes. + +--- + +## 4) Conditional / Dynamic Dependencies + +When different analysis modes need different dependency sets: +```r +if (options[["variant"]] == "classical") { + fitState$dependOn(.classicalDeps) +} else { + fitState$dependOn(.bayesianDeps) +} +``` + +Or combine dynamically: +```r +plot$dependOn(c(.plotDeps, + if (options[["variant"]] == "classical") .classicalDeps else .bayesianDeps +)) +``` + +--- + +## 5) Per-Value Dependencies (optionContainsValue) + +For containers with one child per user-selected variable, invalidate only when that specific variable is removed: + +```r +for (v in options[["variables"]]) { + if (!is.null(container[[v]])) next + plot <- createJaspPlot(title = v) + plot$dependOn(optionContainsValue = list(variables = v)) + container[[v]] <- plot + # ... fill plot ... +} +``` + +If the user removes variable `"x"` from the list, only `container[["x"]]` is NULLed. Other children survive. + +--- + +## 6) Sentinel Pattern (Narrow Dependencies) + +When an expensive computation (e.g., model fit) should NOT be invalidated by visualization-only options, but the visualization data still needs updating: + +```r +# Broad deps: model options → invalidate and re-fit +fitState <- createJaspState() +fitState$dependOn(.modelDeps) +jaspResults[["fit"]] <- fitState + +# Narrow deps: plotting options → update auxiliary data without re-fitting +sentinel <- createJaspState() +sentinel$dependOn(.plottingDeps) +jaspResults[["fitDataUpdate"]] <- sentinel +``` + +When a plotting option changes: +- `jaspResults[["fit"]]` survives (model deps not hit) +- `jaspResults[["fitDataUpdate"]]` is NULLed (plotting deps hit) +- The update function sees the NULL sentinel, re-attaches updated auxiliary data to the existing fit + +This avoids expensive re-computation when only display options change. + +--- + +## 7) Common Pitfalls + +**Missing dependency:** If you forget to list an option in `$dependOn()`, changing that option won't invalidate the element. The user sees stale output. + +**Over-broad dependencies:** Putting ALL options on every element means everything gets recomputed on any change. Split into base deps (container) + specific deps (children). + +**Duplicate dependencies:** Listing a parent container's dep on a child is harmless but redundant. Keep it clean. + +**Forgetting $dependOn entirely:** The element will never be invalidated -- it's created once and persists forever, even when relevant options change. diff --git a/.claude/rules/jasp-module-architecture.md b/.claude/rules/jasp-module-architecture.md new file mode 100644 index 0000000..509ee93 --- /dev/null +++ b/.claude/rules/jasp-module-architecture.md @@ -0,0 +1,284 @@ +--- +paths: + - "**/R/*.R" + - "**/inst/qml/*.qml" +--- + +# JASP Module Architecture + +How QML, JASP Desktop, and R interact. This explains *why* the patterns in the other rule files exist. + +For dependency details see [jasp-dependency-management.md](jasp-dependency-management.md). +For state/caching see [jasp-state-management.md](jasp-state-management.md). +For R coding patterns see [jasp-tables.md](jasp-tables.md), [jasp-plots.md](jasp-plots.md), [jasp-containers-and-errors.md](jasp-containers-and-errors.md). +For serialized output format see [jasp-output-structure.md](jasp-output-structure.md). + +--- + +## 1) The Reactive Loop + +``` +User changes option in QML GUI + │ + ▼ +JASP Desktop collects ALL current option values into a flat named list + │ + ▼ +Desktop calls: AnalysisName(jaspResults, dataset, options) + │ │ │ │ + │ │ │ └─ named list of ALL QML option values + │ │ └─ data.frame loaded from the active dataset + │ └─ PERSISTENT container surviving across invocations + │ + ▼ +R function builds/updates output in jaspResults + │ + ▼ +Desktop reads jaspResults and renders tables/plots/text in the GUI +``` + +**Key insight:** Every time the user changes *anything* in the QML interface, Desktop calls the R analysis function again with a fresh `options` list but the **same** `jaspResults` object. This is why: + +1. Every builder checks `if (!is.null(jaspResults[["key"]])) return()` -- skip if output already exists and dependencies haven't changed. +2. `$dependOn()` tells Desktop which option changes should invalidate (NULL out) an element. See [jasp-dependency-management.md](jasp-dependency-management.md). +3. `createJaspState()` caches expensive computations so they survive across invocations. See [jasp-state-management.md](jasp-state-management.md). + +--- + +## 2) jaspResults: The Persistent Bridge + +`jaspResults` is an R5 reference class that persists between R invocations for the same analysis instance. It is NOT recreated each time. + +### Element lifecycle + +``` +1. Element does not exist → builder creates it, attaches to jaspResults +2. Options change, deps NOT hit → element survives, builder returns early +3. Options change, deps ARE hit → Desktop NULLs the element before calling R + → builder sees NULL, recreates it +4. User removes the analysis → jaspResults is destroyed entirely +``` + +### What can live in jaspResults + +| Create function | Purpose | Displayed? | +|----------------|---------|------------| +| `createJaspTable()` | Tabular output | Yes | +| `createJaspPlot()` | Plot output | Yes | +| `createJaspHtml()` | Raw HTML/text | Yes | +| `createJaspContainer()` | Groups children | Yes (collapsible section) | +| `createJaspState()` | Cache arbitrary R objects | **No** (invisible to user) | + +All five support `$dependOn()`. All five can be stored in jaspResults or nested inside a container. + +### Display ordering + +Every element has `$position` (integer). Lower = higher on page. Children within a container also have positions. + +--- + +## 3) Options: The Flat Named List + +### QML name → R options key + +Every QML control has a `name:` property. Desktop flattens ALL controls into a single named list regardless of QML nesting: + +```qml +CheckBox { + name: "showCI" // options[["showCI"]] = TRUE/FALSE + DoubleField { + name: "ciLevel" // options[["ciLevel"]] = 0.95 + defaultValue: 0.95 + } +} +``` + +Both `showCI` and `ciLevel` appear at the top level of `options`. QML nesting controls UI visibility/enabling but does NOT create nested R structures. + +### Strict option contract + +The QML `name:` value is the backend API. R must read the current QML names directly and every R-read option must be present in the QML-derived options list. + +- Do not add R-side option normalizers, old-name aliases, compatibility maps, or backup defaults for missing GUI options in unreleased work. Missing options should fail so the QML/R mismatch is fixed. +- Do not use `isTRUE(options[["flag"]])` for checkbox options; use `if (options[["flag"]])` so missing keys are not silently treated as `FALSE`. +- Put defaults in QML controls, not in R fallback code. R validation should target `dataset`, `TextField`, and `FormulaField` inputs, not ordinary GUI option defaults. +- Keep `$dependOn()` vectors in current QML names. After renaming options, audit R reads and dependency vectors against `inst/qml/*.qml`. +- Verify every R-read option exists in the QML source that Desktop loads, including imported components. If `jaspTools::analysisOptions()` misses imported components or dynamic values, use a source-aware audit or explicit GUI-equivalent test options; do not inline components, flatten QML, or add R defaults solely for tooling. +- Use `Upgrades.qml` only when preserving compatibility for released analyses with existing saved files. Do not add compatibility layers for new unreleased analyses unless explicitly requested. + +### QML control → R value type + +| QML control | R type | Example value | +|-------------|--------|---------------| +| `CheckBox` | logical | `TRUE` / `FALSE` | +| `DropDown` | character | `"restrictedML"` | +| `RadioButtonGroup` | character | `"estimated"` (selected button's `value:`) | +| `AssignedVariablesList` | character | `"myColumn"` (single) or `c("a","b")` (multi) | +| `DoubleField` | numeric | `0.95` | +| `IntegerField` | integer | `1000L` | +| `TextField` | character | `"user text"` | +| `CIField` | numeric | `0.95` (0-1 scale) | +| `PercentField` | numeric | `95` (0-100 scale) | + +### Empty/unset variable slots + +When no variable is assigned to an `AssignedVariablesList`, the value is `""` (empty string): + +```r +if (options[["dependentVariable"]] != "") { ... } +``` + +For multi-variable lists, check `length(options[["variables"]]) > 0`. + +### Column encoding + +JASP internally encodes column names. In R analysis code, the encoding is transparent -- `dataset` columns are already encoded. Use `jaspBase::decodeColNames()` when displaying names in plot axes/labels. In tests, use `jaspTools:::encodeOptionsAndDataset()` when loading from .jasp files. + +--- + +## 4) Data Flow (Generic) + +``` +QML assigns variable names → options[["dependentVariable"]] = "score" + │ + ▼ +Desktop loads dataset with requested columns → dataset (data.frame) + │ + ▼ +Entry point: readiness check + data validation + - Are required variables assigned? + - .hasErrors(): infinity, observations, variance, etc. + │ + ▼ +Compute function: expensive model fitting, cached in state + - Wrap in try() for error handling + - Store result via createJaspState() + │ + ▼ +Builder functions: extract cached results, build output + - Tables: define columns, build rows, setData() + - Plots: build ggplot, assign to plotObject + - Errors: attach element FIRST, then setError() +``` + +Builders should handle the "not ready" case gracefully -- create empty tables (column headers but no data) so the user sees the output structure before assigning variables. + +--- + +## 5) The Entry Point → Common → Builder Pattern + +### Three-layer architecture + +``` +Layer 1: Entry point (thin wrapper per analysis) + MyAnalysis(jaspResults, dataset, options) + - Sets dispatch flags if sharing code with other analyses + - Validates data + - Delegates to orchestrator + +Layer 2: Orchestrator (flat sequence of builder calls) + MyAnalysisCommon(jaspResults, dataset, options) + - Calls .computeModel() # state + - Calls .summaryTable() # table + - Calls .coefficientsTable() # table + - Calls .mainPlot() # plot + - Conditional sections based on options + +Layer 3: Builders (idempotent, self-contained) + .summaryTable(jaspResults, options) + - Checks if output exists (return early if so) + - Gets/creates container + - Creates table, defines columns + - Extracts cached results + - Builds rows, sets data +``` + +### Multiple entry points sharing one orchestrator + +When related analyses share logic, they set a dispatch flag and delegate: + +```r +AnalysisVariantA <- function(jaspResults, dataset, options) { + options[["variant"]] <- "A" + if (.isReady(options)) { + dataset <- .checkData(dataset, options) + .checkErrors(dataset, options) + } + AnalysisCommon(jaspResults, dataset, options) +} + +AnalysisVariantB <- function(jaspResults, dataset, options) { + options[["variant"]] <- "B" + # ... same pattern ... + AnalysisCommon(jaspResults, dataset, options) +} +``` + +Builders branch on the flag: +```r +if (options[["variant"]] == "B") + .additionalTable(jaspResults, options) +``` + +### The readiness check + +Before model fitting, verify required inputs exist: + +```r +.isReady <- function(options) { + options[["dependentVariable"]] != "" && length(options[["covariates"]]) > 0 +} +``` + +In the entry point: +```r +if (.isReady(options)) { + dataset <- .checkData(dataset, options) + .checkErrors(dataset, options) +} +AnalysisCommon(jaspResults, dataset, options) +``` + +--- + +## 6) Registration & Backward Compatibility + +### Description.qml + +Registers analyses with their R function names: +```qml +Analysis { + title: qsTr("My Analysis") + func: "MyAnalysis" // must match R function name exactly (case-sensitive) +} +``` + +### NAMESPACE + +Every analysis entry point must be exported: +```r +export(MyAnalysis) +``` + +### Upgrades.qml + +For released analyses, when renaming QML option names, add a migration so old .jasp files load correctly. For unreleased analyses, do not add migration or compatibility layers unless explicitly requested; keep only the current QML/R names. +```qml +Upgrade { + functionName: "MyAnalysis" + fromVersion: "0.17.2" + toVersion: "0.17.3" + + ChangeRename { from: "oldOptionName"; to: "newOptionName" } + + ChangeJS { + name: "transformedOption" + jsFunction: function(options) { + switch(options["transformedOption"]) { + case "oldValue": return "newValue"; + default: return options["transformedOption"]; + } + } + } +} +``` diff --git a/.claude/rules/jasp-output-structure.md b/.claude/rules/jasp-output-structure.md new file mode 100644 index 0000000..020d28c --- /dev/null +++ b/.claude/rules/jasp-output-structure.md @@ -0,0 +1,197 @@ +--- +paths: + - "**/tests/testthat/*.R" + - "**/R/*.R" +--- + +# JASP Analysis Output Structure + +Reading and testing the serialized output from `jaspTools::runAnalysis()`. +For building tables see [jasp-tables.md](jasp-tables.md). For plots see [jasp-plots.md](jasp-plots.md). +When you run it manually, use `view = FALSE` so JASP skips HTML generation and you can inspect the returned R object directly. + +## 1) Top-Level `results` Object + +After `jaspTools::runAnalysis(..., view = FALSE)`, the returned list has 5 keys: +- `status` -- `"complete"` or `"fatalError"` +- `results` -- nested list of all output elements (containers, tables, plots) +- `state` -- cached figures and computed objects +- `progress` -- progress info (usually empty after completion) +- `typeRequest` -- internal type info + +## 2) `results$results` Structure + +Contains: +- `.meta` -- recursive metadata describing the tree (type, name, title for each element) +- `name` -- analysis name +- Named elements for each output component (containers, tables, plots) + +### Element Types + +| Type | Key fields | How to identify | +|------|-----------|-----------------| +| **Container** | `collection`, `name`, `title`, `initCollapsed` | Has `$collection` (named list of children) | +| **Table** | `data`, `schema`, `name`, `title`, `status`, `footnotes`, `casesAcrossColumns` | Has `$schema` with `$fields` | +| **Plot/Image** | `data` (string path), `name`, `title`, `width`, `height`, `status`, `convertible` | Has `$data` as character string (e.g., `"plots/1.png"`) | + +## 3) Containers + +Containers group related output elements. Structure: +``` +container$collection -- named list of child elements (containers, tables, or plots) +container$name -- unique identifier (underscore-separated path) +container$title -- display title (can be "") +container$initCollapsed -- whether collapsed by default +``` + +**Naming convention:** Child names are parent name + `_` + child suffix. This creates a hierarchical path: +``` +modelSummaryContainer + modelSummaryContainer_testsTable + modelSummaryContainer_pooledEstimatesTable +``` + +Containers can nest arbitrarily deep: +``` +estimatedMarginalMeansAndContrastsContainer + estimatedMarginalMeansAndContrastsContainer_effectSize + estimatedMarginalMeansAndContrastsContainer_effectSize_adjustedEstimate + ..._adjustedEstimate_estimatedMarginalMeansTable +``` + +**Accessing deeply nested elements:** Chain `$collection` at each container level: +```r +results[["results"]][["containerName"]][["collection"]][["containerName_child"]][["collection"]][["containerName_child_table"]][["data"]] +``` + +## 4) Tables + +### Schema (`table$schema$fields`) +List of column definitions, each with: +- `name` -- field identifier (used as key in data rows) +- `title` -- display column header +- `type` -- `"string"`, `"number"`, `"integer"`, `"pvalue"` +- `format` (optional) -- formatting spec, e.g., `"sf:4;dp:3"`, `"dp:3;p:.001"` +- `overTitle` (optional) -- grouped column header (e.g., `"95% CI"` spanning Lower/Upper) + +### Data (`table$data`) +List of rows. Each row is a named list with field names as keys: +```r +table$data[[1]] # first row +# $est, $se, $lCi, $uCi, $pval, ... +``` + +**Key:** Fields within each row are **alphabetically sorted by name** (from JSON deserialization). + +### Footnotes (`table$footnotes`) +List of footnote objects: +```r +footnote$text -- footnote text +footnote$symbol -- HTML symbol (e.g., "Note.") +footnote$cols -- columns it applies to (NULL = all) +footnote$rows -- rows it applies to (NULL = all) +``` + +### Special Row Fields +- `.isNewGroup` -- boolean, marks visual row separator in JASP GUI +- These appear in `expect_equal_tables` flattened output + +## 5) Plots + +### In `results$results` +Plot entries store metadata only: +```r +plot$data -- string key into state$figures (e.g., "plots/1.png") +plot$name -- identifier +plot$title -- display title +plot$width -- pixel width +plot$height -- pixel height +plot$status -- "complete" +``` + +### In `results$state$figures` +Actual plot objects stored here, keyed by the `data` path: +```r +results$state$figures[["plots/1.png"]]$obj -- the plot object +results$state$figures[["plots/1.png"]]$width +results$state$figures[["plots/1.png"]]$height +``` + +### Plot Object Types +- **`jaspGraphsPlot`** (R6 class) -- composite plot with `$subplots` list of ggplot objects +- **Plain `ggplot`** -- single ggplot object (no subplots) + +### Retrieving Plot for Testing +```r +plotName <- results[["results"]][["plotElement"]][["data"]] +testPlot <- results[["state"]][["figures"]][[plotName]][["obj"]] +jaspTools::expect_equal_plots(testPlot, "snapshot-name") +``` + +## 6) State Object (`results$state`) + +- `state$figures` -- named list of plot objects (keyed by "plots/N.png") +- `state$other` -- named list of cached R objects (keyed by "state_N") + - Used by `createJaspState()` for caching expensive computations between output elements + +## 7) Testing Utilities + +### `expect_equal_tables(table_data, reference_list)` +1. Takes `table$data` (list of row-lists) +2. Flattens via `unname(unlist(rows))` -- row-by-row, fields in alphabetical order within each row +3. Converts numeric strings back to numbers via `charVec2MixedList` +4. Replaces unicode characters with `` placeholder +5. Compares element-by-element against flat reference list + +**Reference list format:** Single flat `list(...)` with all values row-by-row, fields alphabetically sorted: +```r +# For a table with fields: df, est, name, pval (alphabetical) +# Row 1: df=9, est=-0.69, name="Intercept", pval=0.50 +# Row 2: df=9, est=0.29, name="Slope", pval=0.01 +jaspTools::expect_equal_tables(table_data, + list(9, -0.69, "Intercept", 0.50, # row 1 + 9, 0.29, "Slope", 0.01)) # row 2 +``` + +### `expect_equal_plots(plot_obj, snapshot_name)` +- If `jaspGraphsPlot`: splits into subplots, each compared via `vdiffr::expect_doppelganger` with name `"snapshot-name-subplot-N"` +- If plain `ggplot`: compared directly via `vdiffr::expect_doppelganger` +- SVG snapshots stored in `tests/testthat/_snaps/` + +## 8) Quick Reference: Navigating Results + +```r +# Run analysis +results <- jaspTools::runAnalysis("AnalysisName", dataset, options, view = FALSE) + +# Check status +results$status # "complete" or "fatalError" +results$results$errorMessage # if fatalError + +# Get table data (for expect_equal_tables) +results[["results"]][["containerName"]][["collection"]][["containerName_tableName"]][["data"]] + +# Get plot object (for expect_equal_plots) +plotKey <- results[["results"]][["plotName"]][["data"]] +plotObj <- results[["state"]][["figures"]][[plotKey]][["obj"]] + +# Inspect table schema +table$schema$fields # list of {name, title, type, format, overTitle} + +# Map entire tree (debug helper) +mapResults <- function(x, depth = 0) { + indent <- paste(rep(" ", depth), collapse = "") + if (is.list(x) && !is.null(x$collection)) { + cat(sprintf("%s[container] %s: '%s'\n", indent, x$name, x$title)) + for (child in x$collection) mapResults(child, depth + 1) + } else if (is.list(x) && !is.null(x$schema)) { + cat(sprintf("%s[table] %s: '%s' (%d rows x %d cols)\n", + indent, x$name, x$title, length(x$data), length(x$schema$fields))) + } else if (is.list(x) && !is.null(x$data) && is.character(x$data)) { + cat(sprintf("%s[plot] %s: '%s'\n", indent, x$name, x$title)) + } +} +for (item in results$results[setdiff(names(results$results), c(".meta", "name"))]) { + mapResults(item) +} +``` diff --git a/.claude/rules/jasp-plots.md b/.claude/rules/jasp-plots.md new file mode 100644 index 0000000..e1a1735 --- /dev/null +++ b/.claude/rules/jasp-plots.md @@ -0,0 +1,136 @@ +--- +paths: + - "**/R/*.R" +--- + +# JASP Plot Building Patterns + +How to create and configure plots in jaspResults. + +For dependency mechanics see [jasp-dependency-management.md](jasp-dependency-management.md). +For testing plots see [testing-instructions.md](testing-instructions.md) (`expect_equal_plots`). + +--- + +## 1) Simple Plot + +```r +.myPlot <- function(jaspResults, options) { + + if (!is.null(jaspResults[["myPlot"]])) + return() + + fit <- .extractFit(jaspResults, options) + if (is.null(fit) || jaspBase::isTryError(fit[[1]])) + return() + + myPlot <- createJaspPlot( + title = gettext("My Plot"), + width = 400, + height = 320 + ) + myPlot$position <- 5 + myPlot$dependOn(c(.baseDependencies, "plotSpecificOption")) + jaspResults[["myPlot"]] <- myPlot + + # Build ggplot + plotObj <- ggplot2::ggplot(...) + ... + + # Add JASP theme and (plot frame b = bottom, r = right, t = top, l = left) + plotObj <- plotObj + + jaspGraphs::geom_rangeframe(sides = "bl") + + jaspGraphs::themeJaspRaw() + + myPlot$plotObject <- plotObj +} +``` + +--- + +## 2) Plot with Error Handling + +Wrap plot construction in `try()` and display the error on the plot element: + +```r +plotOut <- try(.makePlot(fit, options)) + +if (inherits(plotOut, "try-error")) { + myPlot <- createJaspPlot(title = gettext("My Plot")) + myPlot$dependOn(dependencies) + myPlot$setError(plotOut) + jaspResults[["myPlot"]] <- myPlot + return() +} + +myPlot <- createJaspPlot(title = gettext("My Plot"), width = w, height = h) +myPlot$plotObject <- plotOut +jaspResults[["myPlot"]] <- myPlot +``` + +--- + +## 3) Composite Plot (jaspGraphsPlot) + +For plots with multiple panels (e.g., a left annotation panel + right data panel): + +```r +plotObj <- jaspGraphs:::jaspGraphsPlot$new( + subplots = list(leftPanel, rightPanel), + layout = matrix(1:2, ncol = 2), + heights = 1, + widths = c(0.4, 0.6) +) +myPlot$plotObject <- plotObj +``` + +In tests, each subplot gets its own SVG snapshot: `"name-subplot-1"`, `"name-subplot-2"`. + +--- + +## 4) Per-Group Plot Pattern + +When a single fit produces a single plot, but multiple groups produce a container of plots: + +```r +if (options[["groupingVariable"]] == "") { + # Single plot, attach directly + plot <- .makePlotFun(fit[[1]], options) + plot$title <- gettext("My Plot") + plot$dependOn(dependencies) + jaspResults[["myPlot"]] <- plot + +} else { + # Container with one plot per group + container <- createJaspContainer() + container$title <- gettext("My Plot") + container$dependOn(dependencies) + jaspResults[["myPlot"]] <- container + + for (i in seq_along(fit)) { + container[[names(fit)[i]]] <- .makePlotFun(fit[[i]], options) + container[[names(fit)[i]]]$title <- gettextf("Group: %1$s", attr(fit[[i]], "group")) + container[[names(fit)[i]]]$position <- i + } +} +``` + +--- + +## 5) Separate-Plots-by-Variable Pattern + +When a variable creates multiple faceted plots: + +```r +if (length(options[["separatePlots"]]) > 0) { + container <- createJaspContainer() + for (i in seq_along(levels)) { + tempPlot <- createJaspPlot(title = levels[i], width = w, height = h) + tempPlot$position <- i + tempPlot$plotObject <- makePlot(data[data$facet == levels[i], ]) + container[[paste0("plot", i)]] <- tempPlot + } +} else { + plot <- createJaspPlot(width = w, height = h) + plot$plotObject <- makePlot(data) +} +``` diff --git a/.claude/rules/jasp-state-management.md b/.claude/rules/jasp-state-management.md new file mode 100644 index 0000000..e0f057c --- /dev/null +++ b/.claude/rules/jasp-state-management.md @@ -0,0 +1,257 @@ +--- +paths: + - "**/R/*.R" +--- + +# JASP State Management (createJaspState) + +How to cache expensive computations and track dynamic output state. + +For the reactive loop context see [jasp-module-architecture.md](jasp-module-architecture.md). +For dependency mechanics see [jasp-dependency-management.md](jasp-dependency-management.md). + +Note that you cannot test this by running analysis via `runAnalysis(..., view = FALSE)` because you only generate one state at a time +(with no initial elements - ask the human maintainer to validate the dependencies manually if you suspect an issue!). + +--- + +## 1) Why State Objects Exist + +Model fitting is expensive. Without caching, every option change (even toggling a checkbox for an unrelated table) would re-run the computation. State objects solve this by caching results that persist across R invocations as long as their dependencies hold. + +```r +.computeModel <- function(jaspResults, dataset, options) { + if (!is.null(jaspResults[["modelFit"]])) + return() # cached → skip + + fitState <- createJaspState() + fitState$dependOn(.modelDeps) # only model options + jaspResults[["modelFit"]] <- fitState + + result <- try(expensiveFit(dataset, options)) + fitState$object <- result # cache +} +``` + +Now when the user toggles "Show CI" (a table option, not a model option), `jaspResults[["modelFit"]]` survives. Only when a model option changes does the fit get invalidated and recomputed. + +--- + +## 2) The $object Property + +`createJaspState()` stores arbitrary R objects via `$object`: + +```r +# Store anything: model fits, lists, data.frames +jaspResults[["modelFit"]]$object <- list(model = fitResult, residuals = resid) + +# Retrieve in another builder function +cached <- jaspResults[["modelFit"]]$object +if (is.null(cached)) return() # not yet computed +model <- cached$model +``` + +--- + +## 3) State vs Output Elements + +| | State | Table/Plot/Html | +|---|---|---| +| Visible to user | No | Yes | +| Has `$object` | Yes | No (use `$setData()`, `$plotObject`) | +| Purpose | Cache computations | Display results | +| `$dependOn()` | Yes | Yes | +| Can nest in container | Yes | Yes | + +--- + +## 4) Pattern: Model Fit Caching + +The most common pattern -- fit a model once, reuse across multiple tables and plots: + +```r +.computeModel <- function(jaspResults, dataset, options) { + if (!is.null(jaspResults[["modelFit"]])) + return() + + fitState <- createJaspState() + fitState$dependOn(.modelDeps) + jaspResults[["modelFit"]] <- fitState + + fit <- try(myPackage::fitModel( + formula = .buildFormula(options), + data = dataset + )) + + fitState$object <- fit +} + +# Used by multiple builders: +.extractFit <- function(jaspResults) { + cached <- jaspResults[["modelFit"]]$object + if (is.null(cached)) return(NULL) + return(cached) +} +``` + +--- + +## 5) Pattern: Multiple Fits (Per Group / Per Variable) + +When the analysis computes separate fits for groups or variables, store them as a named list: + +```r +.computeModel <- function(jaspResults, dataset, options) { + if (!is.null(jaspResults[["modelFit"]])) + return() + + fitState <- createJaspState() + fitState$dependOn(.modelDeps) + jaspResults[["modelFit"]] <- fitState + + results <- list() + + # Overall fit + results[["overall"]] <- try(fitFun(dataset, options)) + + # Per-group fits (if grouping variable selected) + if (options[["groupingVariable"]] != "") { + groups <- unique(dataset[[options[["groupingVariable"]]]]) + for (g in groups) { + subData <- dataset[dataset[[options[["groupingVariable"]]]] == g, ] + fit <- try(fitFun(subData, options)) + attr(fit, "group") <- as.character(g) # preserve metadata even on error + results[[paste0("group_", g)]] <- fit + } + } + + fitState$object <- results +} +``` + +**Key conventions:** +- Use `attr(fit, "group")` to tag each fit with its group label (survives `try()` errors) +- Extractors can filter: include/exclude overall, handle errors per group +- Row builders iterate over fits via `lapply()`, returning skeleton data.frames on error + +### Extractor with filtering + +```r +.extractFit <- function(jaspResults, options) { + results <- jaspResults[["modelFit"]]$object + if (is.null(results)) return(NULL) + + # Optionally exclude overall fit + if (options[["groupingVariable"]] != "" && !options[["includeOverall"]]) + results <- results[names(results) != "overall"] + + return(results) +} +``` + +--- + +## 6) Pattern: Shared Computation Cache + +When multiple output elements (table + plot) need the same intermediate result: + +```r +.computeDiagnostics <- function(jaspResults, options) { + if (!is.null(jaspResults[["diagnosticsCache"]])) + return(jaspResults[["diagnosticsCache"]]$object) + + state <- createJaspState() + state$dependOn(.diagnosticsDeps) + jaspResults[["diagnosticsCache"]] <- state + + results <- expensiveComputation(...) + state$object <- results + return(results) +} +``` + +Both `.diagnosticsTable()` and `.diagnosticsPlot()` call `.computeDiagnostics()` -- the second call returns the cached result immediately. + +--- + +## 7) Pattern: Metadata State for Dynamic Containers + +When the set of output children depends on user-selected variables, track what's currently rendered: + +```r +.buildVariableOutputs <- function(jaspResults, options) { + + container <- .extractContainer(jaspResults) + + # Get or create metadata state + if (!is.null(container[["metaData"]])) { + meta <- container[["metaData"]]$object + } else { + metaState <- createJaspState() + metaState$dependOn(c("selectedVariables")) + container[["metaData"]] <- metaState + meta <- list(existing = character(0)) + } + + selected <- options[["selectedVariables"]] + existing <- meta$existing + + # Remove deselected + for (v in setdiff(existing, selected)) + container[[v]] <- NULL + + # Add new + for (v in setdiff(selected, existing)) { + child <- createJaspContainer(title = v) + child$position <- which(selected == v) + container[[v]] <- child + .buildTableForVariable(child, jaspResults, options, v) + } + + # Update tracking + container[["metaData"]]$object <- list(existing = selected) +} +``` + +This avoids rebuilding the entire container when the user adds or removes a single variable. + +--- + +## 8) Pattern: Dataset Update Sentinel + +When an expensive fit should NOT be re-run for visualization-only option changes, but auxiliary data attached to the fit needs updating: + +```r +.updateFitData <- function(jaspResults, dataset, options) { + if (is.null(jaspResults[["modelFit"]])) + return() + if (!is.null(jaspResults[["fitDataUpdate"]])) + return() + + # Create sentinel with narrow deps + sentinel <- createJaspState() + sentinel$dependOn(.plottingVariableDeps) + jaspResults[["fitDataUpdate"]] <- sentinel + + # Update auxiliary data on the existing (cached) fit + fit <- jaspResults[["modelFit"]]$object + fit$plotData <- .prepPlotData(fit, dataset, options) + jaspResults[["modelFit"]]$object <- fit + + sentinel$object <- TRUE # mark as done +} +``` + +When a plotting variable changes: sentinel is NULLed, data is re-attached. The model fit itself survives. + +--- + +## 9) Common Pitfalls + +**Forgetting to store:** Creating a state but never assigning `$object` -- extractors see NULL. + +**Circular extraction:** An extractor that calls the compute function which calls the extractor. Use the `if (!is.null(...)) return()` guard pattern consistently. + +**Overwriting state from extractors:** Extractors should be read-only. Only the compute function should write to `$object`. + +**State without dependencies:** A state with no `$dependOn()` is never invalidated -- it persists forever with potentially stale data. diff --git a/.claude/rules/jasp-tables.md b/.claude/rules/jasp-tables.md new file mode 100644 index 0000000..4bc2b5e --- /dev/null +++ b/.claude/rules/jasp-tables.md @@ -0,0 +1,202 @@ +--- +paths: + - "**/R/*.R" +--- + +# JASP Table Building Patterns + +How to create, configure, and populate tables in jaspResults. + +For dependency mechanics see [jasp-dependency-management.md](jasp-dependency-management.md). +For state/caching see [jasp-state-management.md](jasp-state-management.md). +For containers and error handling see [jasp-containers-and-errors.md](jasp-containers-and-errors.md). + +--- + +## 1) Complete Table Lifecycle + +```r +.myTable <- function(jaspResults, options) { + + container <- .myExtractContainer(jaspResults) + + # 1. SKIP if already created (idempotency) + if (!is.null(container[["myTable"]])) + return() + + fit <- .extractFit(jaspResults, options) + + # 2. CREATE table and attach to parent BEFORE filling data + myTable <- createJaspTable(gettext("My Table Title")) + myTable$position <- 1 + myTable$dependOn(c("optionA", "optionB")) + container[["myTable"]] <- myTable + + # 3. DEFINE columns + myTable$addColumnInfo(name = "term", type = "string", title = "") + myTable$addColumnInfo(name = "est", type = "number", title = gettext("Estimate")) + myTable$addColumnInfo(name = "se", type = "number", title = gettext("Standard Error")) + myTable$addColumnInfo(name = "pval", type = "pvalue", title = gettext("p")) + + # 4. EARLY RETURN on error (table shows as empty with error) + if (is.null(fit)) + return() + if (length(fit) == 1 && jaspBase::isTryError(fit[[1]])) { + myTable$setError(.cleanErrorMessage(fit[[1]])) + return() + } + + # 5. BUILD row data (list of data.frames → rbind) + rows <- do.call(rbind, lapply(fit, .myRowBuilder, options = options)) + + # 6. ADD footnotes + myTable$addFootnote(gettext("Some methodological note.")) + + # 7. SET data + myTable$setData(rows) +} +``` + +**Key**: Always attach the table to jaspResults (step 2) **before** checking errors (step 4). This ensures the empty table with error message displays rather than nothing. See [jasp-containers-and-errors.md](jasp-containers-and-errors.md) for the create-then-error pattern. + +--- + +## 2) Column Types + +| Type | Use for | Format examples | +|------|---------|-----------------| +| `"string"` | Labels, names, formatted test stats | -- | +| `"number"` | Numeric values | `"sf:4;dp:3"` (4 sig figs, 3 decimal places) | +| `"integer"` | Counts, df | -- | +| `"pvalue"` | p-values | `"dp:3;p:.001"` (3 dp, threshold at .001) | + +--- + +## 3) Column Modifiers + +```r +# Grouped column header (e.g., "95% CI" spanning Lower/Upper) +table$addColumnInfo(name = "lCi", type = "number", title = gettext("Lower"), + overtitle = gettextf("%s%% CI", 100 * options[["ciLevel"]])) + +# Show only explicitly added columns (hide data columns not in schema) +table$showSpecifiedColumnsOnly <- TRUE +``` + +--- + +## 4) DRY Pattern: Reusable Column Helpers + +When multiple tables share the same column groups (e.g., CI columns, SE columns, test statistics), factor out repeated `addColumnInfo()` calls into shared helper functions. For example, a helper that conditionally adds a CI lower/upper pair with a dynamic overtitle avoids duplicating those 3-4 lines across every table builder. + +Apply the same pattern for any column group that appears in more than one table — each helper takes the table and relevant options, and adds the columns conditionally. + +--- + +## 5) Parameterized Tables + +When the same table structure serves multiple purposes, parametrize the builder: + +```r +.myTable <- function(jaspResults, options, parameter = "main") { + + container <- .extractContainer(jaspResults) + tableKey <- paste0(parameter, "Table") + + if (!is.null(container[[tableKey]])) + return() + + table <- createJaspTable(switch(parameter, + main = gettext("Main Results"), + summary = gettext("Summary Results") + )) + table$position <- switch(parameter, main = 1, summary = 2) + container[[tableKey]] <- table + # ... columns and data +} +``` + +--- + +## 6) Row Builder Pattern + +Each row builder takes a **single fit** and returns a **data.frame** (one or more rows): + +```r +.myRowBuilder <- function(fit, options) { + + # Handle failed fits gracefully (return skeleton with NAs) + if (jaspBase::isTryError(fit)) { + return(data.frame( + term = gettext("My term"), + group = attr(fit, "group") + )) + } + + row <- data.frame( + term = gettext("My term"), + group = attr(fit, "group"), + est = fit$beta[1], + se = fit$se[1], + pval = fit$pval[1] + ) + + return(row) +} +``` + +**Key conventions:** +- Include `group = attr(fit, "group")` for per-group support +- On error, return data.frame with labels but missing numeric columns (renders as empty cells) +- Use `gettext()` / `gettextf()` for all user-visible strings + +--- + +## 7) DRY Pattern: Safe Data Aggregation + +When combining data.frames from multiple fits — especially when some fits may fail and return fewer columns — create a helper that: + +1. Filters out NULL/empty data.frames +2. Computes the union of all column names +3. Pads each data.frame with NA for missing columns +4. Calls `do.call(rbind, ...)` on the aligned data.frames + +This avoids `rbind()` failures when partial errors produce data.frames with heterogeneous columns. Apply the same helper pattern for ordering rows by grouping variable and simplifying output (e.g., dropping a grouping column when no groups are selected). + +--- + +## 8) Footnotes + +```r +# Simple footnote (appears at bottom) +table$addFootnote(gettext("Fixed effects tested using Knapp and Hartung adjustment.")) + +# Warning-style footnote +table$addFootnote(warningMsg, symbol = gettext("Warning:")) + +# Per-group error footnotes +for (i in which(sapply(fit, jaspBase::isTryError))) { + table$addFootnote( + gettextf("The model for group '%1$s' failed: %2$s", + attr(fit[[i]], "group"), .cleanError(fit[[i]])), + symbol = gettext("Error:") + ) +} + +# Cell-specific footnote +table$addFootnote(message, colNames = "est", rowNames = "rowLabel") +``` + +--- + +## 9) Error Display on Tables + +```r +# Error message replaces entire table content +table$setError(gettext("Feature not available for this model type.")) + +# Error from a try-error object +table$setError(.cleanErrorMessage(tryResult)) +``` + +See [jasp-containers-and-errors.md](jasp-containers-and-errors.md) for the full create-then-error and graceful degradation patterns. diff --git a/.claude/rules/qml-instructions.md b/.claude/rules/qml-instructions.md new file mode 100644 index 0000000..70a9cdf --- /dev/null +++ b/.claude/rules/qml-instructions.md @@ -0,0 +1,179 @@ +--- +paths: + - "**/inst/qml/*.qml" +--- + +# JASP QML Instructions + +## 0) QML Syntax Validation + +**ALWAYS validate QML files after editing** using `qmllint` to catch syntax errors: + +```powershell +qmllint inst\qml\path\to\file.qml +``` + +- **Ignore import warnings**: Warnings about missing `JASP.Controls` and `JASP` modules are expected (qmllint lacks JASP's custom modules) +- **Focus on syntax errors**: Look for missing braces `{}`, brackets `[]`, parentheses `()`, semicolons, or malformed property assignments +- **Exit code matters**: Non-zero exit with syntax errors blocks parsing; zero exit means parseable (even with import warnings) +- **Run before committing**: Catch structural issues (extra/missing braces) that break QML parsing + +Example of ignorable warnings: +``` +Warning: Failed to import JASP.Controls [import] +Warning: IntegerField was not found [import] +``` + +Example of critical errors: +``` +Error: Expected token `}' [syntax] +``` + +## 1) Core Basics + +- **Imports:** + ```qml + import QtQuick + import QtQuick.Layouts + import JASP.Controls + import JASP + ``` + +- **Form as root:** Every analysis UI is a `Form { ... }` containing controls, usually a `VariablesForm` block and option controls. +- **Binding & IDs:** Prefer *property bindings* (reactive JS expressions) over imperative changes; reference other items via `id:` and bind (`enabled: show.checked || useAlt.checked`). +- **Stable storage names:** The `name:` of a control maps to stored options in JASP files; **avoid renaming**. If you must rename a released option, handle migrations in `Upgrades.qml`; for unreleased analyses, keep only the current name. +- **Exact backend API:** Every `name:` is the exact R option key. When renaming an option, update all R `options[["..."]]` reads and `$dependOn()` vectors to the current name; do not rely on R aliases or normalization for unreleased analyses. +- **QML/R option contract:** Every option read by R must be defined in the main QML or an imported component loaded by the GUI. Do not inline reusable components solely because `analysisOptions()` cannot discover them; use source-aware audits/explicit test options and fix real name mismatches. +- **Translation & docs:** + - Wrap **all user-visible strings** in `qsTr("Text")`. + - Populate `info:` with a short, user-facing description (also wrapped in `qsTr`) to feed module help. +- **Variables workflow:** Place variable pickers inside a `VariablesForm`; connect lists with `source:` (can read all data columns, other lists, levels, or R sources). + +## 2) Input Validation + +Prefer **declarative validation** via built-in field properties: + +- **Numeric fields** (`DoubleField`, `IntegerField`): set `min`, `max`, and `inclusive` (e.g., `MinMax`), `decimals` (for doubles), and allow negatives only when needed. Use `fieldWidth` for compact UI. +- **Percent & CI** (`PercentField`, `CIField`): sensible defaults (e.g., 95), `afterLabel` defaults to `"%"`. +- **Slider:** set `min`, `max`, `decimals`; prefer horizontal sliders unless space constrained. +- **FormulaField:** accepts R-style expressions; constrain with `min`, `max`, `inclusive`; use `multiple: true` only when arrays are intended. Read via `realValue` / `realValues`. +- **TableView:** for mixed types, define validators and override `getValidator(col,row)`; optionally specify `itemTypePerRow/Column`. +- **Variables lists:** enforce data types via `allowedColumns: ["scale"|"ordinal"|"nominal"]` and `singleVariable: true` where appropriate. + +## 3) Main Custom Components + +### General input +- **CheckBox** — `name`, `label`, `checked`, `childrenOnSameRow`, `columns` (nested controls auto-enable/disable). +- **RadioButtonGroup / RadioButton** — group has `name`, `title`, `radioButtonsOnSameRow`, `columns`; each button has `value`, `label`, `checked`; can contain nested controls per choice. +- **DropDown** — `name`, `label`, `values` (array or `{label, value}`), or `source`; selection via `startValue` / `currentValue`; `addEmptyValue`, `placeHolderText`. +- **Slider** — `name`, `label`, `value`, `min`, `max`, `decimals`. +- **DoubleField / IntegerField** — `label`, `defaultValue`, `min`, `max`, `inclusive`, (`decimals` for DoubleField). +- **PercentField / CIField** — percent-specific shorthand; defaults appropriate for CIs. +- **TextField** — `defaultValue` or `placeholderText` (mutually exclusive), `afterLabel`, `fieldWidth`. +- **FormulaField** — adds `realValue`, `min/max`, `inclusive`, `multiple`, `realValues`. +- **TextArea** — `title`, `text`, `textType` (e.g., R code / JAGS / Lavaan / Model / Source), `separator(s)`, `applyScriptInfo` (submit with **Ctrl+Enter**). + +### Variable specification +- **AvailableVariablesList** — `name`, `label`, **rich `source`** (other lists, levels, filters, `rSource`, combinations), or `values`; `width`, `count` (read-only). +- **AssignedVariablesList** — `name`, `label`, `allowedColumns`, `singleVariable`, `maxRows`, `listViewType` (e.g., `Interaction`), optional `rowComponent` (+ `rowComponentTitle`), `optionKey`, `count`. +- **FactorLevelList** — define RM factors/levels: `factorName`, `levelName`, `minFactors`, `minLevels`, `width`, `height`. Often paired with an `AssignedVariablesList` of type `MeasuresCells`. + +### Complex composition +- **ComponentsList** — templated rows of controls from a `source` or `values`; `titles`, `rowComponent`, manual rows via `addItemManually`, bounds via `minimumItems` / `maximumItems`, collected under `optionKey`. +- **TabView** — `ComponentsList` rendered as tabs. +- **InputListView** — user adds rows via an input field; `title`, `placeHolder`, `defaultValues`, `minRows`, `inputComponent` (Text/Double/Integer), optional `rowComponent`, `optionKey`. +- **TableView** — `name`, `modelType` (`MultinomialChi2Model`, `JAGSDataInputModel`, `FilteredDataEntryModel`, `CustomContrasts`), `itemType` or per-row/column types, `source`; may override `getColHeaderText`, `getRowHeaderText`, `getDefaultValue`, `getValidator`. + +### Grouping & structure +- **Group** — logical block with `title`, `columns`. Nest options inside. +- **Section** — collapsible panel for advanced options; `title`, `columns`. Use for lower-priority / expert settings. + +## 4) Style & UX Conventions + +- **Titles & labels:** Title Case for section/group titles; concise labels; every visible string uses `qsTr()`. The `name` is always the title transformed into camelCase. Options within groups inherit their names as a prefix. +- **Consistency:** Prefer the provided JASP controls over ad-hoc QML; nest subordinate options inside the control that enables them (e.g., a `CheckBox` containing its dependent fields). +- **Two-column rhythm:** Let the grid flow naturally; use `rowSpan/columnSpan` to avoid awkward gaps; avoid long single-column scrollers. +- **Variables first:** Place `VariablesForm` at the top; align list widths; restrict types with `allowedColumns`. +- **Defaults & placeholders:** Prefer meaningful `defaultValue`; use `placeholderText` only when input is optional. Don't set both. +- **Dropdowns:** Use `{label, value}` pairs when R-side value differs; add an explicit empty choice with `addEmptyValue` if "no selection" is valid. Preserve dynamic `DropDown.values` when they express the intended GUI; use `enabledOptions` only for intended disabled choices, not to expose tooling defaults. +- **Advanced options:** Tuck rare/expert settings into a `Section` titled "Advanced Options". +- **Docs:** Fill `info:` succinctly for every major control. +- **Spacing:** Always use tabs for spacing. Each argument on a new line. (See examples below.) + +## 5) Quick Patterns + +- **Enable dependent field(s):** + ```qml + CheckBox + { + id: show + name: "showX" + label: qsTr("Show X") + } + + DoubleField + { + name: "Alpha" + label: qsTr("Alpha") + defaultValue: 0.05 + min: 0 + max: 1 + decimals: 3 + enabled: show.checked + } + ``` + +- **Radio choice with per-choice inputs:** + ```qml + RadioButtonGroup + { + name: "crit" + title: qsTr("Criterion") + + RadioButton + { + value: "pValue" + label: qsTr("p-value") + checked: true + + DoubleField + { + name: "pValueValue" + label: "" + defaultValue: 0.05 + min: 0 + max: 1 + } + } + + RadioButton + { + ... + } + } + ``` + +- **Variables form (single DV):** + ```qml + VariablesForm + { + AvailableVariablesList + { + name: "availableVariables" + } + + AssignedVariablesList + { + name: "dependentVariable" + label: qsTr("Dependent Variable") + allowedColumns: ["scale"] + singleVariable: true + } + } + ``` + +## 6) When in doubt + +- Prefer built-in JASP controls. +- Keep `name:` stable; translate strings; validate inputs. +- Put rare/expert options in a `Section` and document via `info:`. diff --git a/.claude/rules/r-instructions.md b/.claude/rules/r-instructions.md new file mode 100644 index 0000000..d8b0441 --- /dev/null +++ b/.claude/rules/r-instructions.md @@ -0,0 +1,121 @@ +--- +paths: + - "**/R/*.R" +--- + +# R Instructions + +## 1) Core Basics + +- **Main entry point (name matters):** + - The R function name **must match** the case-sensitive `"function"` field in `Description.qml`. + - Signature is always: + ```r + AnalysisName <- function(jaspResults, dataset, options) { ... } + ``` + - `jaspResults` is a container that stores all of the analysis output and byproducts (if they are supposed to be kept for later use). + - `dataset` is the loaded dataset in JASP + - `options` are the UI choices from QML; **do not rename** option keys (they're your API). + - Read GUI options directly with `options[["name"]]`; do not add R-side normalization, old-name aliases, compatibility maps, or backup defaults for missing QML options in unreleased work. + - For checkbox options, use `if (options[["flag"]])`, not `isTRUE(options[["flag"]])`; `isTRUE()` hides missing/disconnected options by treating them like `FALSE`. + - Keep option names in `$dependOn()` vectors synchronized with current QML `name:` values. + +- **Recommended structure (3 roles):** + 1) **Main function** orchestrates and wires output elements. + 2) **create* functions** declare output markup (tables/plots/text). + 3) **fill* (or compute*) functions** compute results and fill outputs. + +- **Dependencies (cache & reuse):** + Add `$dependOn()` to every output (table/plot/text/container/state) so JASP knows when to reuse or drop it. + Outputs nested within containers inherit all dependencies from the container. + +- **NEVER instantiate jaspResults C++ objects directly** (e.g., `jaspResultsClass$new()`, `create_cpp_jaspResults()`, `jaspBase:::initJaspResults()`). These require JASP Desktop C++ initialization unavailable in headless R sessions. They crash with `Rcpp::not_initialized` or `Expecting an external pointer`. Always use `jaspTools::runAnalysis()` or `agentTestAll()` which handle initialization internally. + +- **Errors:** + - Catch run-time errors with `try(...)` and report via `$setError()`. + - Wrap user-visible text with `gettext()` / `gettextf()` for translation. + +--- + +## 2) Input Validation + +Only validate the `dataset`. `options` input is validated in the QML automatically. +Do not compensate for missing ordinary GUI options in R. Defaults belong in QML controls; if an option is absent, fix the QML/R mapping instead of adding fallback code. The exceptions are targeted validation for arbitrary user text, such as `TextField` and `FormulaField` options. + +Common checks (prefix arguments with the check name): +```r +.hasErrors( + dataset, type = c("factorLevels", "observations", "variance", "infinity", "missingValues"), + factorLevels.target = options$variables, + factorLevels.amount = "< 1", + observations.target = options$variables, + observations.amount = "< 1" +) +``` +Other useful checks: +- `limits.min/max` (inclusive bounds), +- `varCovData.target/corFun` (positive-definiteness), +- `modelInteractions` (ensure lower-order terms exist). + +--- + +## 3) Output Components + +For detailed patterns, examples, and lifecycle guides: + +- Tables: see [jasp-tables.md](jasp-tables.md) +- Plots: see [jasp-plots.md](jasp-plots.md) +- Containers, HTML, errors: see [jasp-containers-and-errors.md](jasp-containers-and-errors.md) +- State/caching: see [jasp-state-management.md](jasp-state-management.md) + +**Quick API reference:** + +| Element | Create | Key properties | +|---------|--------|----------------| +| Table | `createJaspTable(title)` | `$addColumnInfo()`, `$setData(df)`, `$addFootnote()`, `$setError()`, `$showSpecifiedColumnsOnly` | +| Plot | `createJaspPlot(title, width, height)` | `$plotObject <- ggplot(...)`, `$setError()` | +| HTML | `createJaspHtml(text)` | `$text`, `$dependOn()` | +| Container | `createJaspContainer(title)` | `$dependOn()` (propagates to children), nest freely | +| State | `createJaspState()` | `$object` (store/retrieve), `$dependOn()` | + +All elements support `$dependOn()`, `$position`, and `$addCitation()`. + +--- + +## 4) Style & Conventions + +- **Follow the project R style guide.** Keep functions short; prefer pure helpers; avoid global state; no I/O or printing in analyses. +- **Naming:** + - Helpers start with a dot, e.g., `.computeFoo()`, `.fillBarTable()`, `.plotBaz()`. + - Stable keys in `jaspResults[["..."]]` (don't rename them later). +- **Internationalization:** All visible text via `gettext()`/`gettextf()`. +- **Performance:** Read only needed columns; postpone decoding; reuse `createJaspState()` when multiple outputs share results. +- **Robustness:** Validate early; guard long loops with `if (!ready) return()`; wrap risky code in `try()` and call `$setError()`. +- **Reproducibility:** Set column formats explicitly in tables; document assumptions in footnotes/citations. +- **Assignment alignment:** +For related assignments allign them at the arrow `<-`, i.e., +``` +variableOne <- foo() +variableFive <- foo() +``` +and allign function arguments in the similar way for function whose call is too long to be on a single line: +``` +out <- foo( + argumentOne = variableOne, + argumentFive = variableFive, + ... +) + +--- + +## 5) Minimal main() template (copy/paste) + +```r +MyAnalysis <- function(jaspResults, dataset, options) { + + ready <- length(options[["variables"]]) > 0 + + .createMyTable(jaspResults, dataset, options, ready) + .createMyPlot(jaspResults, dataset, options, ready) +} +``` diff --git a/.claude/rules/testing-instructions.md b/.claude/rules/testing-instructions.md new file mode 100644 index 0000000..12b83b0 --- /dev/null +++ b/.claude/rules/testing-instructions.md @@ -0,0 +1,179 @@ +--- +paths: + - "**/tests/testthat/*.R" +--- + +# JASP Testing Instructions + +## 1) Test Framework + +This module uses the `jaspTools` testing framework. Tests are **critical** and must always pass before committing code. + +## 2) Running Tests + +Run via `btw_tool_run_r` in the persistent R session: + +**Agent-optimized** (preferred -- compact output, returns queryable result object): + +```r +# Full test suite -- returns rich S3 result object +x <- agentTestAll() + +# Specific analysis tests +x <- agentTestAnalysis("AnalysisName") +``` + +These return a `jaspAgentTestResults` object with fields: `$status`, `$summary`, `$failures`, `$warnings`, `$skips`, `$tests`, `$errorModules`, `$logFile`. + +**Human-oriented** (verbose output, for interactive use): + +```r +testAll() +testAnalysis("AnalysisName") +``` + +**MCP timeout for large modules:** `agentTestAll()` can take 180-300+ seconds. If `btw_tool_run_r` times out, fall back to Bash: + +```bash +Rscript --no-init-file -e ' + renv::load() + library(jaspTools) + setupJaspTools(pathJaspDesktop="/opt/jasp-desktop", installJaspModules=FALSE, installJaspCorePkgs=FALSE, quiet=TRUE, force=TRUE) + setPkgOption("module.dirs", ".") + setPkgOption("reinstall.modules", FALSE) + agentTestAll() +' +``` + +Do NOT retry via MCP after a timeout -- use the Bash fallback immediately. + +**Critical rules:** + +- Tests take 300+ seconds to complete +- **NEVER CANCEL** tests -- always let them run to completion +- Some deprecation warnings are expected and can be ignored +- ALL tests must pass before proceeding +- Some tests skip on certain platforms (e.g., Windows) -- this is expected +- Some stderr noise (ggplot messages, tryCatch errors) may leak through -- expected and minor + +## 3) Test File Structure + +Each test file in `tests/testthat/` corresponds to an R analysis file: + +- `test-penalizedmetaanalysis.R` -> `R/penalizedmetaanalysis.R` +- Test file name pattern: `test-.R` +- Analysis names for `agentTestAnalysis()` come from NAMESPACE exports (PascalCase) + +## 4) Writing Tests + +### Basic test structure + +```r +# 1. Set up analysis options +options <- jaspTools::analysisOptions("AnalysisName") +options$variables <- "contGamma" +options$descriptives <- TRUE + +# 2. Set seed for reproducibility +set.seed(1) + +# 3. Run the analysis +results <- jaspTools::runAnalysis("AnalysisName", "debug.csv", options, view = FALSE) + +# 4. Test tables +test_that("Table name matches", { + table <- results[["results"]][["tableName"]][["data"]] + jaspTools::expect_equal_tables(table, list(...expected values...)) +}) + +# 5. Test plots +test_that("Plot name matches", { + plotName <- results[["results"]][["containerName"]][["collection"]][["plotId"]][["data"]] + testPlot <- results[["state"]][["figures"]][[plotName]][["obj"]] + jaspTools::expect_equal_plots(testPlot, "plotname", dir = "AnalysisName") +}) +``` + +### Loading from .jasp example files + +```r +jaspFile <- testthat::test_path("..", "..", "examples", "Example Name.jasp") +opts <- jaspTools::analysisOptions(jaspFile) +dataset <- jaspTools::extractDatasetFromJASPFile(jaspFile) +encoded <- jaspTools:::encodeOptionsAndDataset(opts, dataset) +set.seed(1) +results <- jaspTools::runAnalysis("AnalysisName", encoded$dataset, encoded$options, encodedDataset = TRUE, view = FALSE) +``` + +### Key testing functions + +- `jaspTools::analysisOptions(name)` -- Get default options for an analysis +- `jaspTools::runAnalysis(name, dataset, options, view = FALSE)` -- Run analysis without generating HTML; inspect the returned R object +- `jaspTools::expect_equal_tables(actual, expected)` -- Compare table output +- `jaspTools::expect_equal_plots(plot, name, dir)` -- Compare plot output (snapshot-based) + +## 5) Test Data + +- `"debug.csv"` is a built-in jaspTools dataset containing most data types +- Use `set.seed()` before running analyses for reproducibility +- Example .jasp files in `examples/` provide pre-configured options and datasets + +## 6) Test Snapshots + +- Snapshots stored in `tests/testthat/_snaps/` +- **NEVER automatically accept snapshot changes** -- always notify user for manual inspection +- When a new snapshot is created, inform the user so they can verify it + +## 7) When to Update Tests + +### Always update tests when + +1. Adding new analysis outputs (tables, plots, text) +2. Modifying existing output structure or values +3. Adding new QML options that affect results +4. Changing analysis calculations + +### How to update test expectations + +1. Run tests and capture new output +2. Verify the new output is correct +3. Update expected values in test file +4. Re-run tests to confirm they pass + +## 8) Test Workflow + +### Before making code changes + +Run `agentTestAll()` via `btw_tool_run_r` to establish baseline -- all tests should pass. + +### After making code changes + +1. Run `devtools::load_all()` to hot-reload R changes +2. Run `agentTestAnalysis("AnalysisName")` for quick iteration on the affected analysis +3. Once the specific tests pass, run `agentTestAll()` to check for regressions + +### If tests fail + +1. Review the failure messages carefully +2. Check if failure is expected (due to your intentional changes) +3. If expected: update test expectations and notify user about snapshot changes +4. If unexpected: fix your code +5. Re-run tests until all pass + +## 9) Adding New Tests + +When adding a new analysis: + +1. Create test file: `tests/testthat/test-.R` +2. Set up options with all default values explicitly set +3. Test all output tables and plots +4. Test edge cases and error conditions +5. Use meaningful variable names and test data + +## 10) Best Practices + +- **One test per output element** -- separate `test_that()` blocks for each table/plot +- **Descriptive test names** -- clearly state what is being tested +- **Reproducible** -- always use `set.seed()` for analyses with randomness +- **Complete option coverage** -- test with various option combinations +- **Keep tests focused** -- each test should verify one specific aspect diff --git a/.claude/rules/translation-instructions.md b/.claude/rules/translation-instructions.md new file mode 100644 index 0000000..6cc4892 --- /dev/null +++ b/.claude/rules/translation-instructions.md @@ -0,0 +1,256 @@ +--- +paths: + - "**/R/*.R" + - "**/inst/qml/*.qml" + - "**/po/**" +--- + +# Translation (i18n) Instructions + +## 1) Core Principle + +**ALL user-visible text must be wrapped for translation.** + +This module is translated into multiple languages via Weblate integration. + +## 2) R Code Translation + +### Use `gettext()` for static strings: +```r +# Single string +message <- gettext("Analysis complete") + +# Table titles +tab <- createJaspTable(title = gettext("Descriptive Statistics")) + +# Error messages +tab$setError(gettext("Insufficient observations")) +``` + +### Use `gettextf()` for dynamic strings: +```r +# Single placeholder +msg <- gettextf("Variable %s has insufficient data", varName) + +# Multiple placeholders - use numbered format for translators +msg <- gettextf("Number of factor levels is %1$s in %2$s", nLevels, varName) + +# Percentage signs must be doubled +label <- gettextf("%s%% CI for Mean Difference", 100 * alpha) +``` + +### Use `ngettext()` for plurals: +```r +msg <- ngettext(n, + "One observation removed", + "%d observations removed", + domain = "R-jaspEquivalenceTTests") +``` + +### Column overtitles with dynamic content: +```r +if (options$confidenceInterval) { + ciLabel <- gettextf("%s%% CI", 100 * options$confidenceIntervalLevel) + tab$addColumnInfo("lower", gettext("Lower"), overtitle = ciLabel) + tab$addColumnInfo("upper", gettext("Upper"), overtitle = ciLabel) +} +``` + +## 3) QML Translation + +### Wrap all visible strings with `qsTr()`: +```qml +CheckBox +{ + name: "descriptives" + label: qsTr("Descriptive statistics") + + CheckBox + { + name: "confidenceInterval" + label: qsTr("Confidence interval") + info: qsTr("Display confidence intervals for effect sizes") + } +} +``` + +### For groups and sections: +```qml +Group +{ + title: qsTr("Additional Statistics") + + CheckBox + { + label: qsTr("Effect size") + } +} + +Section +{ + title: qsTr("Advanced Options") + + DoubleField + { + label: qsTr("Prior scale") + } +} +``` + +### Radio buttons and dropdowns: +```qml +RadioButtonGroup +{ + name: "hypothesis" + title: qsTr("Alternative Hypothesis") + + RadioButton + { + value: "twoSided" + label: qsTr("Two-sided") + } + + RadioButton + { + value: "greater" + label: qsTr("Greater than") + } +} + +DropDown +{ + name: "effectSize" + label: qsTr("Effect Size") + values: [ + { label: qsTr("Cohen's d"), value: "cohen" }, + { label: qsTr("Glass' delta"), value: "glass" } + ] +} +``` + +## 4) Translation Rules + +### DO wrap for translation: +- ✅ Table/plot/container titles +- ✅ Column names and overtitles +- ✅ Error messages and warnings +- ✅ Footnotes and citations +- ✅ All QML labels, titles, and info text +- ✅ Help text and descriptions +- ✅ Button labels and tooltips + +### DON'T wrap for translation: +- ❌ Empty strings: `""` (NEVER mark for translation) +- ❌ Variable names (internal identifiers) +- ❌ Statistical symbols: `"β"`, `"p"`, `"t"`, `"df"` +- ❌ Mathematical expressions +- ❌ Code or syntax +- ❌ File paths + +### Format specifications: +```r +# CORRECT - use numbered placeholders for clarity +gettextf("Mean difference is %1$s with SE = %2$s", mean, se) + +# AVOID - unnamed placeholders are harder for translators +gettextf("Mean difference is %s with SE = %s", mean, se) +``` + +### Special characters: +```r +# Use UTF-8 escape sequences for non-ASCII +label <- gettext("Cram\u00E9r's V") # Cramér's V +symbol <- gettext("\u03B2") # β (beta) +``` + +### Percentage signs in format strings: +```r +# WRONG - single % will cause format error +label <- gettextf("%s% CI", 95) + +# CORRECT - double %% in format string +label <- gettextf("%s%% CI", 95) +``` + +## 5) Translation Workflow + +### Automated process: +1. Developers write code with `gettext()`/`gettextf()`/`qsTr()` +2. Translation extraction happens automatically +3. Weblate platform provides translation interface +4. Translators work on Weblate +5. Translation files synced back to repository automatically +6. `.github/workflows/translations.yml` handles automation + +### Translation files location: +``` +po/ # R translation files +inst/qml/translations/ # QML translation files (if exists) +``` + +### Manual updates (rare): +Usually handled automatically, but if needed: +```bash +# Update R translations (done by translation workflow) +# Don't manually edit .po files unless absolutely necessary +``` + +## 6) Testing Translations + +While we can't easily test all languages locally, ensure: +1. All user-visible strings are wrapped +2. Format strings use numbered placeholders +3. Percentage signs are doubled in format strings +4. No empty strings marked for translation +5. Context provided for ambiguous terms + +## 7) Common Mistakes to Avoid + +### ❌ WRONG: +```r +# Missing translation +tab <- createJaspTable(title = "Descriptive Statistics") + +# Empty string marked for translation +label <- gettext("") + +# Unnamed placeholders +msg <- gettextf("Found %s issues in %s", count, name) + +# Single % for percentage +label <- gettextf("%s% Confidence Interval", 95) +``` + +### ✅ CORRECT: +```r +# Proper translation +tab <- createJaspTable(title = gettext("Descriptive Statistics")) + +# No translation for empty string +label <- "" + +# Numbered placeholders for translators +msg <- gettextf("Found %1$s issues in %2$s", count, name) + +# Doubled %% for percentage +label <- gettextf("%s%% Confidence Interval", 95) +``` + +## 8) Translation Context + +For ambiguous terms, consider adding comments: +```r +# "Mean" as in average (not "mean" as in unkind) +columnTitle <- gettext("Mean") + +# "Scale" as in measurement scale (not fish scales) +fieldLabel <- qsTr("Scale variable") +``` + +## 9) Weblate Integration + +- Weblate repo: `jaspequivalencettests-qml` and `jaspequivalencettests-r` +- Automated workflow: `.github/workflows/translations.yml` +- Scheduled runs: Weekly on Saturday at 2:45 AM +- Manual trigger: `workflow_dispatch` available +- Translation updates automatically create commits/PRs diff --git a/.claude/session_startup.R b/.claude/session_startup.R new file mode 100644 index 0000000..af4d177 --- /dev/null +++ b/.claude/session_startup.R @@ -0,0 +1,50 @@ +# JASP Module - R Session Startup for Claude Code +# Run this script in your interactive R session (RStudio/Positron/radian) +# to prepare and hand over the session to Claude Code. +# +# Usage: source(".claude/session_startup.R") + +# Fix cli::get_spinner() conflict with testthat in btw/evaluate context. +# BOTH the option AND the monkey-patch are needed: +# - options(cli.spinner = "line") sets a sane default +# - But btw:::local_reproducible_output() overrides it to FALSE on every +# btw_tool_run_r() call, causing cli::get_spinner() to hit: +# FALSE$frames -> "$ operator is invalid for atomic vectors" +# - The monkey-patch intercepts logical values and coerces to "line" +# Upstream fixes pending: posit-dev/btw and r-lib/cli +options(cli.spinner = "line") +local({ + original_get_spinner <- cli::get_spinner + patched_get_spinner <- function(which = NULL) { + if (is.null(which)) { + opt <- getOption("cli.spinner") + if (identical(opt, FALSE) || identical(opt, TRUE)) { + options(cli.spinner = "line") + } + } else if (identical(which, FALSE) || identical(which, TRUE)) { + which <- "line" + } + original_get_spinner(which = which) + } + utils::assignInNamespace("get_spinner", patched_get_spinner, ns = "cli") +}) + +# Fix locale issue with renv.lock files created on non-English systems +if (.Platform$OS.type == "windows") { + Sys.setlocale("LC_ALL", "English_United States.utf8") +} else { + Sys.setlocale("LC_ALL", "C.UTF-8") +} + +# Install order matches container_entrypoint.sh: +# 1. Install injected packages (btw, mcptools) first +# 2. renv::restore() so lockfile-pinned versions win for shared deps +# 3. Install the module + jaspTools (already handled by restore for deps) +renv::install(c("btw", "mcptools"), prompt = FALSE) +renv::restore(prompt = FALSE) +renv::install(c(".", "jasp-stats/jaspTools"), prompt = FALSE) +library(jaspTools) +setupJaspTools() +setPkgOption("module.dirs", ".") +setPkgOption("reinstall.modules", FALSE) +btw::btw_mcp_session() diff --git a/.claude/skills/fix-debug-analysis.md b/.claude/skills/fix-debug-analysis.md new file mode 100644 index 0000000..3719901 --- /dev/null +++ b/.claude/skills/fix-debug-analysis.md @@ -0,0 +1,427 @@ +# Fix & Debug JASP Analysis (MCP Session) + +Quick reference for debugging JASP analysis functions through MCP sessions. + +**Note**: `browser()` and `recover()` require interactive R console and **do not work** through MCP's `btw_tool_run_r`. + +--- + +## 1) Debugging Approaches + +There are two approaches, in order of preference: + +### Approach A: Code Inspection (try first) + +Many bugs — especially logic errors, missing branches, wrong conditions — are solvable by reading the code and tracing the control flow. This is faster and doesn't require instrumenting code. + +1. **Reproduce**: Bootstrap a `runAnalysis()` call (Step 0) and confirm the issue +2. **Read**: Trace the code path from the entry-point function through the relevant helpers +3. **Identify**: Look for logic errors — wrong conditions, missing option checks, incorrect branching +4. **Fix**: Edit the source, hot-reload, and verify + +**Use this when**: Output is missing, wrong options are checked, a feature works in one analysis type but not another, UI options don't match R-side logic. + +### Approach B: saveRDS State Capture (escalation) + +When the bug depends on runtime values that can't be deduced from code reading alone. + +1. **Instrument**: Add saveRDS() before the error location +2. **Capture**: Hot-reload and run analysis, copy debug path from console +3. **Inspect**: Load saved state and examine values via MCP +4. **Fix**: Develop and test fix using captured state +5. **Verify**: Remove debug code, hot-reload, confirm fix works + +**Use this when**: Error depends on specific data values, unexpected NULL/type, dimension mismatches, or the code path is too complex to trace by reading. + +--- + +## 2) Reproducing the Issue + +### Step 0: Bootstrap a Reproducible Analysis Run + +Before debugging, you need a working `runAnalysis()` call that reproduces the error. Choose the first applicable source: + +#### Option A: User provides a .jasp file + +```r +jaspFile <- "path/to/file.jasp" +opts <- jaspTools::analysisOptions(jaspFile) +dataset <- jaspTools::extractDatasetFromJASPFile(jaspFile) +encoded <- jaspTools:::encodeOptionsAndDataset(opts, dataset) +set.seed(1) +results <- jaspTools::runAnalysis("AnalysisName", encoded$dataset, encoded$options, encodedDataset = TRUE, view = FALSE) +``` + +If the .jasp file contains multiple analyses, `analysisOptions()` returns a list — index with `[[1]]`, `[[2]]`, etc. Pick the analysis that matches the error context. + +#### Option B: Extract from existing unit tests (most common fallback) + +When no .jasp file is provided, **search test files first**. Test files contain pre-configured options and dataset references that are known to produce complete output. + +1. **Find the test file** for the analysis in `tests/testthat/`: + ``` + grep -r "AnalysisName" tests/testthat/ + ``` + +2. **Determine the input pattern** used in the test. Tests use one of two patterns: + + **Pattern 1 — .jasp example file** (look for `analysisOptions(jaspFile)` or `extractDatasetFromJASPFile`): + ```r + # Copy the loading code from the test, adjusting the path for non-test context + jaspFile <- file.path("examples", "Example Name.jasp") + opts <- jaspTools::analysisOptions(jaspFile)[[1]] # note: may need [[1]] for multi-analysis files + dataset <- jaspTools::extractDatasetFromJASPFile(jaspFile) + encoded <- jaspTools:::encodeOptionsAndDataset(opts, dataset) + set.seed(1) + results <- jaspTools::runAnalysis("AnalysisName", encoded$dataset, encoded$options, encodedDataset = TRUE, view = FALSE) + ``` + + **Pattern 2 — inline options** (look for `analysisOptions("AnalysisName")` with explicit option assignments): + ```r + # Copy the options setup from the test verbatim + options <- jaspTools::analysisOptions("AnalysisName") + options$dependent <- "contNormal" # copy from test + options$group <- "contBinom" # copy from test + # ... copy ALL option assignments from the test ... + set.seed(1) + results <- jaspTools::runAnalysis("AnalysisName", "debug.csv", options, view = FALSE) + ``` + +3. **Modify options** to match the bug-triggering scenario (e.g., enable/disable specific checkboxes). + +4. **Verify reproduction**: Check that the issue is reproduced — this could be a `"fatalError"` status, an error message in a specific output element, incorrect values, missing output, etc., depending on what the user reported. + +#### Option C: Build options from scratch (last resort) + +Only when no tests or examples exist: + +```r +options <- jaspTools::analysisOptions("AnalysisName") +# Set required inputs — check .robttCheckReady() or equivalent readiness function +# to discover which options must be non-empty +options$dependent <- "contNormal" +options$group <- "contBinom" +set.seed(1) +results <- jaspTools::runAnalysis("AnalysisName", "debug.csv", options, view = FALSE) +``` + +**Tip**: `jaspTools::analysisOptions("AnalysisName")` returns all options with their QML defaults. Inspect it with `str(options)` to understand available options and their types. + +--- + +## 3) saveRDS Workflow (Approach B) + +Use these steps when code inspection alone is insufficient and you need to examine runtime values. + +### Step 1: Identify Error Location + +From the error message and stack trace, locate the function and approximate line where the error occurs. + +**Example**: Stack trace shows `.buildTable()` → `table$addFootnote()` → error + +### Step 2: Instrument Code + +Add saveRDS() just **before** the line that's failing: + +```r +.buildTable <- function(jaspResults, options) { + # ... existing code ... + + someVariable <- computeSomething(data, options) + + # DEBUG: REMOVE - save state before error + debug_dir <- tempdir() + saveRDS(list( + someVariable = someVariable, + relatedData = relatedData, + fit = fit, + options = options + # Include ALL relevant variables + ), file.path(debug_dir, "debug_state.rds")) + message("DEBUG: Saved to ", file.path(debug_dir, "debug_state.rds")) + + # The line that's failing + processData(someVariable) +} +``` + +**Critical rules**: +- Always use marker comment `# DEBUG: REMOVE` +- **Never save `jaspResults`** (crashes R) +- Save to `tempdir()` (auto-cleanup) +- Include `message()` to print path to console +- Save ALL variables that might be relevant + +### Step 3: Hot-Reload and Capture + +```r +# Via btw_tool_run_r in MCP +devtools::load_all() + +# Re-run the analysis (use same code that triggered original error) +set.seed(1) +results <- jaspTools::runAnalysis("AnalysisName", encoded$dataset, encoded$options, encodedDataset = TRUE, view = FALSE) + +# Console output will show: +# DEBUG: Saved to C:/Users/.../Temp/RtmpXXX/debug_state.rds +``` + +Copy the debug path from the console output. + +### Step 4: Inspect Captured State + +```r +# Via btw_tool_run_r in MCP +debug_path <- "C:/Users/.../Temp/RtmpXXX/debug_state.rds" +debug_data <- readRDS(debug_path) + +# Examine structure +str(debug_data) + +# Inspect specific variables +print(debug_data$someVariable) +sapply(debug_data$someVariable, class) +any(sapply(debug_data$someVariable, is.null)) + +# Check attributes +for (i in seq_along(debug_data$relatedData)) { + cat("Item", i, "attribute:", attr(debug_data$relatedData[[i]], "someAttr"), "\n") +} +``` + +**Goal**: Identify the exact values causing the error. + +### Step 5: Develop Fix + +Based on inspection, develop fix logic using the saved objects: + +```r +# Via btw_tool_run_r in MCP +# Test the fix logic interactively using saved state + +# Example: Filter out invalid values +someVariable_clean <- Filter(function(x) !is.null(x) && is.finite(x), debug_data$someVariable) +print(someVariable_clean) # Verify it works + +# Try the fix +for (i in seq_along(someVariable_clean)) { + cat("Would process item:", someVariable_clean[[i]], "\n") +} +``` + +Once fix logic works, implement it in the source file. + +### Step 6: Clean Up and Verify + +1. Apply fix to source file +2. **Remove all debug code** (saveRDS(), message(), and "# DEBUG: REMOVE" markers) +3. Hot-reload and verify: + +```r +# Via btw_tool_run_r in MCP +devtools::load_all() + +set.seed(1) +results <- jaspTools::runAnalysis("AnalysisName", encoded$dataset, encoded$options, encodedDataset = TRUE, view = FALSE) + +# Check overall status +cat("Status:", results$status, "\n") +``` + +4. **Verify the specific issue is resolved** — don't just check `results$status`: + - If the bug was missing output: confirm the output element now exists in `results$results` + - If the bug was wrong values: check the specific table/cell values + - If the bug was an error in a subcomponent: navigate to that component and verify no error + - If the bug was a crash: confirm status is `"complete"` + +5. Search for any remaining debug code before committing: + +```bash +grep -r "DEBUG: REMOVE" R/ +grep -r "saveRDS.*tempdir" R/ +``` + +--- + +## 4) What to Save + +| Location | Objects to save | DON'T save | +|----------|----------------|------------| +| **Model fitting** | `dataset`, `options`, function args, intermediate values | `jaspResults`, `...` (ellipsis args) | +| **Row building** | `fit`, `attr(fit, "group")`, computed rows, `options` | Parent containers, environments | +| **Table assembly** | `rows` list, intermediate data.frames | Full fit objects if not needed | +| **Error handling** | Error object, variables being processed when error occurred | Large intermediate objects | + +**Golden rule**: When unsure, save it. Missing a variable means re-running the entire capture process. + +--- + +## 5) Real-World Example + +**Error**: `jaspTable$addFootnote expects 'message' to be a string!` + +**Workflow**: + +1. **Loaded .jasp file and reproduced error**: + ```r + jaspFile <- "path/to/file.jasp" + opts <- jaspTools::analysisOptions(jaspFile) + dataset <- jaspTools::extractDatasetFromJASPFile(jaspFile) + encoded <- jaspTools:::encodeOptionsAndDataset(opts, dataset) + set.seed(1) + results <- jaspTools::runAnalysis("AnalysisName", encoded$dataset, encoded$options, encodedDataset = TRUE, view = FALSE) + # → Status: fatalError + ``` + +2. **Identified error location**: Stack trace → `.buildTable()` at specific line + +3. **Instrumented code**: + ```r + footnotes <- unique(lapply(dataList, attr, which = "footnote")) + + # DEBUG: REMOVE + saveRDS(list( + footnotes = footnotes, + dataList = dataList + ), file.path(tempdir(), "footnote_debug.rds")) + message("DEBUG: Saved to ", file.path(tempdir(), "footnote_debug.rds")) + + for (i in seq_along(footnotes)) + table$addFootnote(footnotes[[i]]) + ``` + +4. **Captured state**: + ```r + devtools::load_all() + results <- jaspTools::runAnalysis(..., view = FALSE) + # Console: DEBUG: Saved to C:/Users/.../RtmpXXX/footnote_debug.rds + ``` + +5. **Inspected**: + ```r + debug_data <- readRDS("C:/Users/.../RtmpXXX/footnote_debug.rds") + str(debug_data$footnotes) + # List of 2 + # $ : chr "Some footnote text..." + # $ : NULL ← THE PROBLEM + ``` + +6. **Root cause**: `unique()` preserves NULL values → loop called `addFootnote(NULL)` → error + +7. **Implemented fix**: + ```r + footnotes <- unique(lapply(dataList, attr, which = "footnote")) + footnotes <- Filter(Negate(is.null), footnotes) # Filter NULLs + for (i in seq_along(footnotes)) + table$addFootnote(footnotes[[i]]) + ``` + +8. **Verified**: + ```r + devtools::load_all() + results <- jaspTools::runAnalysis(..., view = FALSE) + # → Status: complete ✓ + ``` + +**Time**: ~5 minutes from error to verified fix. + +--- + +## 6) Advanced Techniques + +### Conditional Saving + +For errors in specific iterations/groups: + +```r +# Only save when condition is met +for (i in seq_along(items)) { + if (i == 47) { # Error only in iteration 47 + saveRDS(list(item = items[[i]], i = i), file.path(tempdir(), "debug_iter47.rds")) + message("DEBUG: Saved iteration 47") + } + result <- process(items[[i]]) +} +``` + +### Multiple Checkpoints + +Narrow down error location by saving at multiple points: + +```r +# Checkpoint 1 +saveRDS(list(step = "before_transform", data = data), + file.path(tempdir(), "checkpoint1.rds")) + +data_transformed <- transform(data) + +# Checkpoint 2 +saveRDS(list(step = "after_transform", data_transformed = data_transformed), + file.path(tempdir(), "checkpoint2.rds")) +``` + +### Save with Timestamp + +For multiple runs: + +```r +timestamp <- format(Sys.time(), "%Y%m%d_%H%M%S") +saveRDS(list(...), file.path(tempdir(), paste0("debug_", timestamp, ".rds"))) +``` + +--- + +## 7) Common Error Patterns + +### Pattern 1: Unexpected NULL + +**Symptom**: "argument is NULL" or "expects X to be a Y" + +**Debugging**: +```r +saveRDS(list(suspect_var = suspect_var, related_vars = list(...)), ...) +# Inspect: is.null(debug_data$suspect_var) +``` + +### Pattern 2: Wrong Type/Class + +**Symptom**: "cannot coerce X to Y" or "is not a valid type" + +**Debugging**: +```r +saveRDS(list(var = var, class = class(var), str = capture.output(str(var))), ...) +# Inspect: class(debug_data$var), attributes(debug_data$var) +``` + +### Pattern 3: Dimension Mismatch + +**Symptom**: "dims [product X] do not match length of object [Y]" + +**Debugging**: +```r +saveRDS(list(obj = obj, dims = dim(obj), length = length(obj)), ...) +# Inspect: dim(debug_data$obj), length(debug_data$obj) +``` + +### Pattern 4: Index Out of Bounds + +**Symptom**: "subscript out of bounds" or "undefined columns selected" + +**Debugging**: +```r +saveRDS(list(container = container, index = i, length = length(container)), ...) +# Inspect: i vs length(debug_data$container), names(debug_data$container) +``` + +--- + +## 8) Safety Checklist + +Before committing code: + +- [ ] All `# DEBUG: REMOVE` markers removed +- [ ] All `saveRDS()` calls removed +- [ ] All debug `message()` calls removed +- [ ] Verified with: `grep -r "DEBUG: REMOVE" R/` +- [ ] Verified with: `grep -r "saveRDS.*tempdir" R/` +- [ ] Hot-reloaded and tested: analysis completes successfully diff --git a/.codex/README.md b/.codex/README.md new file mode 100644 index 0000000..ac25b0c --- /dev/null +++ b/.codex/README.md @@ -0,0 +1,85 @@ +# Codex CLI Instructions + +This directory contains project-specific configuration for OpenAI Codex CLI. + +## Structure + +``` +.codex/ +├── README.md # This file +├── config.toml # MCP servers, sandbox, approval settings +└── rules/ + ├── default.rules # Starlark execution policy (git safety) + ├── r-instructions.md # R backend guidelines + ├── qml-instructions.md # QML interface guidelines + ├── testing-instructions.md # Test framework guidelines + ├── git-workflow.md # Git and commit conventions + ├── translation-instructions.md # i18n/l10n guidelines + ├── jasp-module-architecture.md # QML-Desktop-R reactive loop + ├── jasp-dependency-management.md # $dependOn mechanics + ├── jasp-state-management.md # createJaspState caching + ├── jasp-tables.md # Table building patterns + ├── jasp-plots.md # Plot building patterns + ├── jasp-containers-and-errors.md # Container patterns, error handling + └── jasp-output-structure.md # Serialized output format + +.agents/ +└── skills/ + └── fix-debug-analysis/ + └── SKILL.md # Debugging skill (cross-platform) +``` + +## Setup + +### 1. Trust the project + +On first launch, Codex prompts to trust the project. Accept to load `.codex/config.toml` and `.codex/rules/`. + +### 2. MCP Server Configuration + +MCP servers are configured in `.codex/config.toml` and load automatically. The R MCP server uses the shared script at `.claude/mcp-server.R`. + +### 3. R Session Setup + +Before starting a Codex session, run in your interactive R console: + +```r +source(".claude/session_startup.R") +``` + +This restores dependencies, installs the module, configures jaspTools, and registers the session. Then connect via `list_r_sessions` / `select_r_session` in Codex. + +### 4. Using Skills + +The `fix-debug-analysis` skill is available at `.agents/skills/fix-debug-analysis/SKILL.md`. Invoke explicitly via `$fix-debug-analysis` or let Codex auto-trigger it when debugging tasks are detected. + +## How It Works + +**AGENTS.md** (project root) is automatically loaded at session start. It contains the main project instructions and references to rule files in `.codex/rules/`. + +**Rule files** in `.codex/rules/` are NOT auto-loaded by path pattern (Codex doesn't support path-scoping). Instead, `AGENTS.md` instructs Codex to read the relevant rule file when working on matching file types. + +**Execution policy** in `.codex/rules/default.rules` uses Starlark syntax to gate shell commands (e.g., forbid force-push, prompt before push). + +**config.toml** configures MCP servers, sandbox mode, and approval policy. Shared between Codex CLI and the IDE extension. + +## Differences from Claude Code + +| Feature | Claude Code (`.claude/`) | Codex CLI (`.codex/`) | +|---------|--------------------------|------------------------| +| Main instructions | `CLAUDE.md` (auto-loaded) | `AGENTS.md` (auto-loaded) | +| Rule files | `.claude/rules/*.md` with `paths:` frontmatter (auto-scoped) | `.codex/rules/*.md` (referenced explicitly from AGENTS.md) | +| Skills | `.claude/skills/*.md` | `.agents/skills/*/SKILL.md` | +| MCP config | `.mcp.json` (JSON) | `.codex/config.toml` (TOML) | +| Permissions | `settings.local.json` (granular per-tool) | `config.toml` sandbox + approval | +| Hooks | `hooks/block-test-edits.js` (PreToolUse) | Not available (instruction-only) | +| Execution policy | Not available | `.codex/rules/default.rules` (Starlark) | +| Config format | JSON | TOML | + +## Copying to Other JASP Modules + +1. Copy `AGENTS.md` to the target module root +2. Copy `.codex/` directory to the target module +3. Copy `.agents/` directory to the target module +4. The `.claude/mcp-server.R` and `.claude/session_startup.R` scripts are shared and should already exist +5. Adjust paths in `config.toml` if the MCP server script location differs diff --git a/.codex/config.toml b/.codex/config.toml new file mode 100644 index 0000000..6d5f27e --- /dev/null +++ b/.codex/config.toml @@ -0,0 +1,19 @@ +# Codex CLI project configuration for JASP modules +# Requires: project must be trusted for this config to load +# See: https://developers.openai.com/codex/config-reference/ + +sandbox_mode = "workspace-write" +approval_policy = "on-request" + +# Increase project doc limit to accommodate comprehensive AGENTS.md +project_doc_max_bytes = 65536 + +# Reasoning: use high effort for complex module-fixing tasks +model_reasoning_effort = "high" +model_reasoning_summary = "concise" + +[mcp_servers.r-mcptools] +command = "Rscript" +args = ["-e", "source('.claude/mcp-server.R')"] +startup_timeout_sec = 60 +tool_timeout_sec = 600 diff --git a/.codex/rules/default.rules b/.codex/rules/default.rules new file mode 100644 index 0000000..fed8cc6 --- /dev/null +++ b/.codex/rules/default.rules @@ -0,0 +1,97 @@ +# Execution policy for JASP module development +# See: https://developers.openai.com/codex/exec-policy + +# Allow safe read-only git commands +prefix_rule( + pattern = ["git", ["status", "diff", "log", "branch", "show", "stash", "tag", "remote"]], + decision = "allow", + justification = "Read-only git operations are safe.", + match = [ + "git status", + "git diff", + "git log --oneline -5", + "git branch", + ], +) + +# Allow grep/find for code search +prefix_rule( + pattern = ["grep"], + decision = "allow", + justification = "Code search is safe.", +) + +prefix_rule( + pattern = ["find"], + decision = "allow", + justification = "File search is safe.", +) + +# Allow Rscript execution (MCP server, tests, analysis runs) +prefix_rule( + pattern = ["Rscript"], + decision = "allow", + justification = "R execution needed for MCP server, tests, and analysis.", +) + +# Allow qmllint for QML validation +prefix_rule( + pattern = ["qmllint"], + decision = "allow", + justification = "QML syntax validation is safe and required before commits.", +) + +# Forbid force push +prefix_rule( + pattern = ["git", "push", ["-f", "--force", "--force-with-lease"]], + decision = "forbidden", + justification = "Force pushing is forbidden per git-workflow.md. Use normal push.", + match = [ + "git push -f origin main", + "git push --force origin feature/test", + "git push --force-with-lease origin main", + ], + not_match = [ + "git push origin feature/test", + ], +) + +# Forbid destructive git reset +prefix_rule( + pattern = ["git", "reset", "--hard"], + decision = "forbidden", + justification = "Hard reset can lose work. Use 'git stash' or 'git checkout' instead.", + match = [ + "git reset --hard HEAD", + "git reset --hard origin/master", + ], +) + +# Forbid working on master directly +prefix_rule( + pattern = ["git", "checkout", ["master", "main"]], + decision = "prompt", + justification = "Switching to master/main requires approval. Always work on feature branches.", + match = [ + "git checkout master", + "git checkout main", + ], +) + +# Prompt before any git push (requires human approval) +prefix_rule( + pattern = ["git", "push"], + decision = "prompt", + justification = "Pushing to remote requires explicit human approval per git-workflow.md.", + match = [ + "git push origin feature/test", + "git push -u origin feature/test", + ], +) + +# Prompt before git commit (ensure tests were run) +prefix_rule( + pattern = ["git", "commit"], + decision = "prompt", + justification = "Verify all tests pass before committing.", +) diff --git a/.codex/rules/git-workflow.md b/.codex/rules/git-workflow.md new file mode 100644 index 0000000..1b9a3b9 --- /dev/null +++ b/.codex/rules/git-workflow.md @@ -0,0 +1,198 @@ +## Commit Message Style + +**Be extremely concise. Sacrifice grammar for concision.** + +### Format: +``` +: + +[optional body if needed] + +Co-Authored-By: Claude Sonnet 4.5 +``` + +### Types: +- `feat:` - New feature or analysis +- `fix:` - Bug fix +- `refactor:` - Code restructuring without behavior change +- `test:` - Adding or updating tests +- `docs:` - Documentation only +- `i18n:` - Translation updates +- `chore:` - Maintenance tasks + +### Examples: +``` +feat: add equivalence bounds plot + +fix: correct CI calculation in paired t-test + +test: update snapshots for descriptives table + +refactor: extract common validation logic + +i18n: update translation files +``` + +## Commit Workflow + +### 0. Ensure on feature branch: +```bash +# Check current branch +git branch + +# If on master, create feature branch +git checkout -b feature/descriptive-name +``` + +### 1. Before committing: +```bash +# Run full test suite +Rscript -e "library(jaspTools); agentTestAll()" + +# Check git status +git status + +# Review changes +git diff +``` + +### 2. Stage specific files: +```bash +# Stage specific files (preferred) +git add R/equivalenceonesamplettest.R +git add tests/testthat/test-equivalenceonesamplettest.R + +# Avoid staging everything unless you're certain +# git add -A # Be careful with this +``` + +### 3. Commit locally with co-author: +```bash +git commit -m "$(cat <<'EOF' +feat: add descriptives table + +Co-Authored-By: Claude Sonnet 4.5 +EOF +)" +``` + +**Local commits are OK. Pushing to remote requires human approval.** + +## Pre-Commit Requirements + +Before every commit, ensure: +- ✅ All tests pass (`jaspTools::agentTestAll()`) +- ✅ No unintended files staged (.env, credentials, etc.) +- ✅ Commit message is concise and descriptive +- ✅ Changes are focused and related + +## Branch Strategy + +- **Main branch:** `master` +- **NEVER work directly on `master` branch** +- **ALWAYS create a feature branch for any changes:** + ```bash + git checkout -b feature/descriptive-name + ``` +- Branch naming conventions: + - `feature/description` - New features or analyses + - `fix/description` - Bug fixes + - `refactor/description` - Code restructuring + - `test/description` - Test updates + +## Pull Request Guidelines + +**CRITICAL: NEVER push to remote, create PRs, or merge without explicit human approval.** + +Human must review all local changes before they go online. + +When human approves creating a PR: +1. Ensure all tests pass locally first +2. Keep PR scope focused and small +3. Use concise PR title (same style as commits) +4. Summarize changes in bullet points +5. Note any breaking changes +6. Wait for human to review the PR description before posting + +## What NOT to Commit + +- ❌ `.Rhistory`, `.RData`, `.Rproj.user/` +- ❌ Test artifacts or temporary files +- ❌ Personal IDE settings +- ❌ Large data files +- ❌ Credentials or API keys +- ❌ `CLAUDE.local.md` (personal preferences) + +## CI/CD Integration + +- GitHub Actions runs tests on every push +- Workflow file: `.github/workflows/unittests.yml` +- Tests must pass for PR to be merged +- Translation workflows run on schedule + +## Git Safety + +- **NEVER** work directly on `master` branch - always use feature branches +- **NEVER** push to remote without explicit human approval +- **NEVER** create pull requests without explicit human approval +- **NEVER** merge changes without explicit human approval +- **NEVER** force push to any branch +- **NEVER** amend published commits +- **NEVER** skip hooks unless explicitly needed +- **NEVER** commit without running tests first + +**Human must approve all changes before they go online.** + +## Common Git Commands + +```bash +# Check current branch +git branch + +# Create and switch to feature branch +git checkout -b feature/description + +# Check status +git status + +# View changes +git diff +git diff --staged + +# Stage specific files +git add + +# Commit locally (OK to do without approval) +git commit -m "message" + +# View recent commits +git log --oneline -5 + +# View commit history with graph +git log --graph --oneline --all -10 + +# === REQUIRE HUMAN APPROVAL BEFORE RUNNING: === + +# Push to remote (WAIT FOR APPROVAL) +git push origin feature/description + +# Pull latest changes (usually safe, but confirm first) +git pull origin master +``` + +## Handling Test Failures + +If CI tests fail after human has pushed: +1. Check GitHub Actions output +2. Reproduce failure locally +3. Fix the issue +4. Run tests to confirm fix +5. Commit locally +6. Ask human for approval to push fix + +## Translation Commits + +Translation updates are handled automatically: +- Weblate integration updates translation files +- Automated commits from translation workflow +- Don't manually edit translation files unless necessary diff --git a/.codex/rules/jasp-containers-and-errors.md b/.codex/rules/jasp-containers-and-errors.md new file mode 100644 index 0000000..b404c9d --- /dev/null +++ b/.codex/rules/jasp-containers-and-errors.md @@ -0,0 +1,141 @@ +# JASP Containers, HTML Output & Error Handling + +Patterns for grouping output elements and handling errors in jaspResults. + +For tables see [jasp-tables.md](jasp-tables.md). +For plots see [jasp-plots.md](jasp-plots.md). +For state/caching see [jasp-state-management.md](jasp-state-management.md). + +--- + +## 1) Containers + +Containers group related output elements under a collapsible section. + +### Get-or-create pattern (reusable across multiple builder functions) + +```r +.myExtractContainer <- function(jaspResults) { + if (!is.null(jaspResults[["myContainer"]])) + return(jaspResults[["myContainer"]]) + + container <- createJaspContainer(gettext("My Section Title")) + container$dependOn(.myBaseDependencies) + container$position <- 1 + jaspResults[["myContainer"]] <- container + + return(container) +} +``` + +- Use a dedicated extractor when **multiple builder functions** write to the same container +- `$position` controls display order (lower = higher on page) +- `$dependOn()` on the container invalidates **all children** when base options change + +### Direct creation (when only one function writes to it) + +```r +if (is.null(jaspResults[["sectionContainer"]])) { + container <- createJaspContainer(gettext("Section Title")) + container$dependOn(c(.baseDependencies, "specificOption")) + container$position <- 4 + jaspResults[["sectionContainer"]] <- container +} +``` + +### Nested containers + +For deeply hierarchical output (e.g., per-variable tables): + +```r +outerContainer <- jaspResults[["outer"]] +innerContainer <- createJaspContainer(title = "Variable X") +innerContainer$position <- i +outerContainer[["variableX"]] <- innerContainer +# then add tables/plots to innerContainer +``` + +### Dynamic container management + +When the set of children depends on user-selected variables: + +```r +# Track existing vs selected variables via metadata state +existingVariables <- metaData[["existingVariables"]] +selectedVariables <- getSelectedVariables(options) + +# Remove deselected +for (v in setdiff(existingVariables, selectedVariables)) + container[[v]] <- NULL + +# Add new +for (v in setdiff(selectedVariables, existingVariables)) { + childContainer <- createJaspContainer(title = v) + container[[v]] <- childContainer + .buildChildTable(childContainer, fit, options, v) +} + +# Update metadata +metaDataState$object <- list(existingVariables = selectedVariables) +``` + +See [jasp-state-management.md](jasp-state-management.md) for the metadata state pattern that powers this. + +--- + +## 2) HTML Output + +For raw HTML content (e.g., displaying R code or formatted messages): + +```r +htmlOutput <- createJaspHtml(title = gettext("R Code")) +htmlOutput$dependOn(c(.baseDependencies, "showCode")) +htmlOutput$position <- 99 +htmlOutput$text <- "
myFunction(yi = ..., sei = ...)
" +jaspResults[["rCode"]] <- htmlOutput +``` + +--- + +## 3) Error Handling Patterns + +### Create-then-error + +Always **attach the element to jaspResults before checking errors**. This ensures the empty table (with error message) is displayed rather than nothing: + +```r +table <- createJaspTable(gettext("Title")) +container[["table"]] <- table # attach FIRST + +# THEN check for errors +if (someError) { + table$setError(errorMessage) + return() +} +``` + +### Graceful degradation with groups + +When some per-group fits fail but others succeed, show partial results with per-group error footnotes: + +```r +# Row builders return skeleton data.frames on error (labels only, NAs for numeric columns) +# Tables show partial results with error footnotes per failed group +for (i in which(sapply(fit, jaspBase::isTryError))) { + table$addFootnote( + gettextf("Group '%1$s' failed: %2$s", attr(fit[[i]], "group"), .cleanError(fit[[i]])), + symbol = gettext("Error:") + ) +} +``` + +### Total failure + +When the entire fit fails: + +```r +if (length(fit) == 1 && jaspBase::isTryError(fit[[1]])) { + table$setError(.cleanErrorMessage(fit[[1]])) + return() +} +``` diff --git a/.codex/rules/jasp-dependency-management.md b/.codex/rules/jasp-dependency-management.md new file mode 100644 index 0000000..b7dce0c --- /dev/null +++ b/.codex/rules/jasp-dependency-management.md @@ -0,0 +1,131 @@ +# JASP Dependency Management ($dependOn) + +How `$dependOn()` controls caching and invalidation of output elements in jaspResults. + +For the reactive loop context see [jasp-module-architecture.md](jasp-module-architecture.md). + +Note that you cannot test this by running analysis via `runAnalysis(..., view = FALSE)` because you only generate one state at a time +(with no initial elements - ask the human maintainer to validate the dependencies manually if you suspect an issue!). + +--- + +## 1) What $dependOn Does + +When you write: +```r +table$dependOn(c("method", "ciLevel")) +``` + +You tell JASP Desktop: "If `options[["method"]]` or `options[["ciLevel"]]` changes, set this element to NULL before calling R." On the next R invocation, the builder's `if (!is.null(...))` guard sees NULL and recreates the element. + +Elements whose dependencies are NOT hit survive across invocations -- the builder returns early and the existing output stays on screen. + +--- + +## 2) Dependency Inheritance + +Container dependencies propagate to ALL children: + +```r +container$dependOn(c("dependentVariable", "method")) # base deps +table$dependOn(c("showCI")) # additional dep +container[["myTable"]] <- table +``` + +The table is invalidated if `dependentVariable`, `method`, OR `showCI` changes. Never repeat parent deps on children. + +This means you can put shared model-level dependencies on the container and only add output-specific deps to individual tables/plots. + +--- + +## 3) Dependency Vectors as Constants + +Define at file top for reuse across builders: +```r +.baseDeps <- c("dependentVariable", "covariates", "method", "ciLevel") +.plotDeps <- c("plotColor", "plotSize", "plotTheme") +``` + +Use in builders: +```r +container$dependOn(.baseDeps) # container holds base deps +table$dependOn(c("showResiduals")) # child adds specific dep +plot$dependOn(c(.baseDeps, .plotDeps)) # or combine for standalone elements +``` + +Keep dependency vectors comprehensive -- missing a dependency means stale output when that option changes. + +--- + +## 4) Conditional / Dynamic Dependencies + +When different analysis modes need different dependency sets: +```r +if (options[["variant"]] == "classical") { + fitState$dependOn(.classicalDeps) +} else { + fitState$dependOn(.bayesianDeps) +} +``` + +Or combine dynamically: +```r +plot$dependOn(c(.plotDeps, + if (options[["variant"]] == "classical") .classicalDeps else .bayesianDeps +)) +``` + +--- + +## 5) Per-Value Dependencies (optionContainsValue) + +For containers with one child per user-selected variable, invalidate only when that specific variable is removed: + +```r +for (v in options[["variables"]]) { + if (!is.null(container[[v]])) next + plot <- createJaspPlot(title = v) + plot$dependOn(optionContainsValue = list(variables = v)) + container[[v]] <- plot + # ... fill plot ... +} +``` + +If the user removes variable `"x"` from the list, only `container[["x"]]` is NULLed. Other children survive. + +--- + +## 6) Sentinel Pattern (Narrow Dependencies) + +When an expensive computation (e.g., model fit) should NOT be invalidated by visualization-only options, but the visualization data still needs updating: + +```r +# Broad deps: model options → invalidate and re-fit +fitState <- createJaspState() +fitState$dependOn(.modelDeps) +jaspResults[["fit"]] <- fitState + +# Narrow deps: plotting options → update auxiliary data without re-fitting +sentinel <- createJaspState() +sentinel$dependOn(.plottingDeps) +jaspResults[["fitDataUpdate"]] <- sentinel +``` + +When a plotting option changes: +- `jaspResults[["fit"]]` survives (model deps not hit) +- `jaspResults[["fitDataUpdate"]]` is NULLed (plotting deps hit) +- The update function sees the NULL sentinel, re-attaches updated auxiliary data to the existing fit + +This avoids expensive re-computation when only display options change. + +--- + +## 7) Common Pitfalls + +**Missing dependency:** If you forget to list an option in `$dependOn()`, changing that option won't invalidate the element. The user sees stale output. + +**Over-broad dependencies:** Putting ALL options on every element means everything gets recomputed on any change. Split into base deps (container) + specific deps (children). + +**Duplicate dependencies:** Listing a parent container's dep on a child is harmless but redundant. Keep it clean. + +**Forgetting $dependOn entirely:** The element will never be invalidated -- it's created once and persists forever, even when relevant options change. diff --git a/.codex/rules/jasp-module-architecture.md b/.codex/rules/jasp-module-architecture.md new file mode 100644 index 0000000..57b7d1b --- /dev/null +++ b/.codex/rules/jasp-module-architecture.md @@ -0,0 +1,278 @@ +# JASP Module Architecture + +How QML, JASP Desktop, and R interact. This explains *why* the patterns in the other rule files exist. + +For dependency details see [jasp-dependency-management.md](jasp-dependency-management.md). +For state/caching see [jasp-state-management.md](jasp-state-management.md). +For R coding patterns see [jasp-tables.md](jasp-tables.md), [jasp-plots.md](jasp-plots.md), [jasp-containers-and-errors.md](jasp-containers-and-errors.md). +For serialized output format see [jasp-output-structure.md](jasp-output-structure.md). + +--- + +## 1) The Reactive Loop + +``` +User changes option in QML GUI + │ + ▼ +JASP Desktop collects ALL current option values into a flat named list + │ + ▼ +Desktop calls: AnalysisName(jaspResults, dataset, options) + │ │ │ │ + │ │ │ └─ named list of ALL QML option values + │ │ └─ data.frame loaded from the active dataset + │ └─ PERSISTENT container surviving across invocations + │ + ▼ +R function builds/updates output in jaspResults + │ + ▼ +Desktop reads jaspResults and renders tables/plots/text in the GUI +``` + +**Key insight:** Every time the user changes *anything* in the QML interface, Desktop calls the R analysis function again with a fresh `options` list but the **same** `jaspResults` object. This is why: + +1. Every builder checks `if (!is.null(jaspResults[["key"]])) return()` -- skip if output already exists and dependencies haven't changed. +2. `$dependOn()` tells Desktop which option changes should invalidate (NULL out) an element. See [jasp-dependency-management.md](jasp-dependency-management.md). +3. `createJaspState()` caches expensive computations so they survive across invocations. See [jasp-state-management.md](jasp-state-management.md). + +--- + +## 2) jaspResults: The Persistent Bridge + +`jaspResults` is an R5 reference class that persists between R invocations for the same analysis instance. It is NOT recreated each time. + +### Element lifecycle + +``` +1. Element does not exist → builder creates it, attaches to jaspResults +2. Options change, deps NOT hit → element survives, builder returns early +3. Options change, deps ARE hit → Desktop NULLs the element before calling R + → builder sees NULL, recreates it +4. User removes the analysis → jaspResults is destroyed entirely +``` + +### What can live in jaspResults + +| Create function | Purpose | Displayed? | +|----------------|---------|------------| +| `createJaspTable()` | Tabular output | Yes | +| `createJaspPlot()` | Plot output | Yes | +| `createJaspHtml()` | Raw HTML/text | Yes | +| `createJaspContainer()` | Groups children | Yes (collapsible section) | +| `createJaspState()` | Cache arbitrary R objects | **No** (invisible to user) | + +All five support `$dependOn()`. All five can be stored in jaspResults or nested inside a container. + +### Display ordering + +Every element has `$position` (integer). Lower = higher on page. Children within a container also have positions. + +--- + +## 3) Options: The Flat Named List + +### QML name → R options key + +Every QML control has a `name:` property. Desktop flattens ALL controls into a single named list regardless of QML nesting: + +```qml +CheckBox { + name: "showCI" // options[["showCI"]] = TRUE/FALSE + DoubleField { + name: "ciLevel" // options[["ciLevel"]] = 0.95 + defaultValue: 0.95 + } +} +``` + +Both `showCI` and `ciLevel` appear at the top level of `options`. QML nesting controls UI visibility/enabling but does NOT create nested R structures. + +### Strict option contract + +The QML `name:` value is the backend API. R must read the current QML names directly and every R-read option must be present in the QML-derived options list. + +- Do not add R-side option normalizers, old-name aliases, compatibility maps, or backup defaults for missing GUI options in unreleased work. Missing options should fail so the QML/R mismatch is fixed. +- Do not use `isTRUE(options[["flag"]])` for checkbox options; use `if (options[["flag"]])` so missing keys are not silently treated as `FALSE`. +- Put defaults in QML controls, not in R fallback code. R validation should target `dataset`, `TextField`, and `FormulaField` inputs, not ordinary GUI option defaults. +- Keep `$dependOn()` vectors in current QML names. After renaming options, audit R reads and dependency vectors against `inst/qml/*.qml`. +- Verify every R-read option exists in the QML source that Desktop loads, including imported components. If `jaspTools::analysisOptions()` misses imported components or dynamic values, use a source-aware audit or explicit GUI-equivalent test options; do not inline components, flatten QML, or add R defaults solely for tooling. +- Use `Upgrades.qml` only when preserving compatibility for released analyses with existing saved files. Do not add compatibility layers for new unreleased analyses unless explicitly requested. + +### QML control → R value type + +| QML control | R type | Example value | +|-------------|--------|---------------| +| `CheckBox` | logical | `TRUE` / `FALSE` | +| `DropDown` | character | `"restrictedML"` | +| `RadioButtonGroup` | character | `"estimated"` (selected button's `value:`) | +| `AssignedVariablesList` | character | `"myColumn"` (single) or `c("a","b")` (multi) | +| `DoubleField` | numeric | `0.95` | +| `IntegerField` | integer | `1000L` | +| `TextField` | character | `"user text"` | +| `CIField` | numeric | `0.95` (0-1 scale) | +| `PercentField` | numeric | `95` (0-100 scale) | + +### Empty/unset variable slots + +When no variable is assigned to an `AssignedVariablesList`, the value is `""` (empty string): + +```r +if (options[["dependentVariable"]] != "") { ... } +``` + +For multi-variable lists, check `length(options[["variables"]]) > 0`. + +### Column encoding + +JASP internally encodes column names. In R analysis code, the encoding is transparent -- `dataset` columns are already encoded. Use `jaspBase::decodeColNames()` when displaying names in plot axes/labels. In tests, use `jaspTools:::encodeOptionsAndDataset()` when loading from .jasp files. + +--- + +## 4) Data Flow (Generic) + +``` +QML assigns variable names → options[["dependentVariable"]] = "score" + │ + ▼ +Desktop loads dataset with requested columns → dataset (data.frame) + │ + ▼ +Entry point: readiness check + data validation + - Are required variables assigned? + - .hasErrors(): infinity, observations, variance, etc. + │ + ▼ +Compute function: expensive model fitting, cached in state + - Wrap in try() for error handling + - Store result via createJaspState() + │ + ▼ +Builder functions: extract cached results, build output + - Tables: define columns, build rows, setData() + - Plots: build ggplot, assign to plotObject + - Errors: attach element FIRST, then setError() +``` + +Builders should handle the "not ready" case gracefully -- create empty tables (column headers but no data) so the user sees the output structure before assigning variables. + +--- + +## 5) The Entry Point → Common → Builder Pattern + +### Three-layer architecture + +``` +Layer 1: Entry point (thin wrapper per analysis) + MyAnalysis(jaspResults, dataset, options) + - Sets dispatch flags if sharing code with other analyses + - Validates data + - Delegates to orchestrator + +Layer 2: Orchestrator (flat sequence of builder calls) + MyAnalysisCommon(jaspResults, dataset, options) + - Calls .computeModel() # state + - Calls .summaryTable() # table + - Calls .coefficientsTable() # table + - Calls .mainPlot() # plot + - Conditional sections based on options + +Layer 3: Builders (idempotent, self-contained) + .summaryTable(jaspResults, options) + - Checks if output exists (return early if so) + - Gets/creates container + - Creates table, defines columns + - Extracts cached results + - Builds rows, sets data +``` + +### Multiple entry points sharing one orchestrator + +When related analyses share logic, they set a dispatch flag and delegate: + +```r +AnalysisVariantA <- function(jaspResults, dataset, options) { + options[["variant"]] <- "A" + if (.isReady(options)) { + dataset <- .checkData(dataset, options) + .checkErrors(dataset, options) + } + AnalysisCommon(jaspResults, dataset, options) +} + +AnalysisVariantB <- function(jaspResults, dataset, options) { + options[["variant"]] <- "B" + # ... same pattern ... + AnalysisCommon(jaspResults, dataset, options) +} +``` + +Builders branch on the flag: +```r +if (options[["variant"]] == "B") + .additionalTable(jaspResults, options) +``` + +### The readiness check + +Before model fitting, verify required inputs exist: + +```r +.isReady <- function(options) { + options[["dependentVariable"]] != "" && length(options[["covariates"]]) > 0 +} +``` + +In the entry point: +```r +if (.isReady(options)) { + dataset <- .checkData(dataset, options) + .checkErrors(dataset, options) +} +AnalysisCommon(jaspResults, dataset, options) +``` + +--- + +## 6) Registration & Backward Compatibility + +### Description.qml + +Registers analyses with their R function names: +```qml +Analysis { + title: qsTr("My Analysis") + func: "MyAnalysis" // must match R function name exactly (case-sensitive) +} +``` + +### NAMESPACE + +Every analysis entry point must be exported: +```r +export(MyAnalysis) +``` + +### Upgrades.qml + +For released analyses, when renaming QML option names, add a migration so old .jasp files load correctly. For unreleased analyses, do not add migration or compatibility layers unless explicitly requested; keep only the current QML/R names. +```qml +Upgrade { + functionName: "MyAnalysis" + fromVersion: "0.17.2" + toVersion: "0.17.3" + + ChangeRename { from: "oldOptionName"; to: "newOptionName" } + + ChangeJS { + name: "transformedOption" + jsFunction: function(options) { + switch(options["transformedOption"]) { + case "oldValue": return "newValue"; + default: return options["transformedOption"]; + } + } + } +} +``` diff --git a/.codex/rules/jasp-output-structure.md b/.codex/rules/jasp-output-structure.md new file mode 100644 index 0000000..0b9bed8 --- /dev/null +++ b/.codex/rules/jasp-output-structure.md @@ -0,0 +1,191 @@ +# JASP Analysis Output Structure + +Reading and testing the serialized output from `jaspTools::runAnalysis()`. +For building tables see [jasp-tables.md](jasp-tables.md). For plots see [jasp-plots.md](jasp-plots.md). +When you run it manually, use `view = FALSE` so JASP skips HTML generation and you can inspect the returned R object directly. + +## 1) Top-Level `results` Object + +After `jaspTools::runAnalysis(..., view = FALSE)`, the returned list has 5 keys: +- `status` -- `"complete"` or `"fatalError"` +- `results` -- nested list of all output elements (containers, tables, plots) +- `state` -- cached figures and computed objects +- `progress` -- progress info (usually empty after completion) +- `typeRequest` -- internal type info + +## 2) `results$results` Structure + +Contains: +- `.meta` -- recursive metadata describing the tree (type, name, title for each element) +- `name` -- analysis name +- Named elements for each output component (containers, tables, plots) + +### Element Types + +| Type | Key fields | How to identify | +|------|-----------|-----------------| +| **Container** | `collection`, `name`, `title`, `initCollapsed` | Has `$collection` (named list of children) | +| **Table** | `data`, `schema`, `name`, `title`, `status`, `footnotes`, `casesAcrossColumns` | Has `$schema` with `$fields` | +| **Plot/Image** | `data` (string path), `name`, `title`, `width`, `height`, `status`, `convertible` | Has `$data` as character string (e.g., `"plots/1.png"`) | + +## 3) Containers + +Containers group related output elements. Structure: +``` +container$collection -- named list of child elements (containers, tables, or plots) +container$name -- unique identifier (underscore-separated path) +container$title -- display title (can be "") +container$initCollapsed -- whether collapsed by default +``` + +**Naming convention:** Child names are parent name + `_` + child suffix. This creates a hierarchical path: +``` +modelSummaryContainer + modelSummaryContainer_testsTable + modelSummaryContainer_pooledEstimatesTable +``` + +Containers can nest arbitrarily deep: +``` +estimatedMarginalMeansAndContrastsContainer + estimatedMarginalMeansAndContrastsContainer_effectSize + estimatedMarginalMeansAndContrastsContainer_effectSize_adjustedEstimate + ..._adjustedEstimate_estimatedMarginalMeansTable +``` + +**Accessing deeply nested elements:** Chain `$collection` at each container level: +```r +results[["results"]][["containerName"]][["collection"]][["containerName_child"]][["collection"]][["containerName_child_table"]][["data"]] +``` + +## 4) Tables + +### Schema (`table$schema$fields`) +List of column definitions, each with: +- `name` -- field identifier (used as key in data rows) +- `title` -- display column header +- `type` -- `"string"`, `"number"`, `"integer"`, `"pvalue"` +- `format` (optional) -- formatting spec, e.g., `"sf:4;dp:3"`, `"dp:3;p:.001"` +- `overTitle` (optional) -- grouped column header (e.g., `"95% CI"` spanning Lower/Upper) + +### Data (`table$data`) +List of rows. Each row is a named list with field names as keys: +```r +table$data[[1]] # first row +# $est, $se, $lCi, $uCi, $pval, ... +``` + +**Key:** Fields within each row are **alphabetically sorted by name** (from JSON deserialization). + +### Footnotes (`table$footnotes`) +List of footnote objects: +```r +footnote$text -- footnote text +footnote$symbol -- HTML symbol (e.g., "Note.") +footnote$cols -- columns it applies to (NULL = all) +footnote$rows -- rows it applies to (NULL = all) +``` + +### Special Row Fields +- `.isNewGroup` -- boolean, marks visual row separator in JASP GUI +- These appear in `expect_equal_tables` flattened output + +## 5) Plots + +### In `results$results` +Plot entries store metadata only: +```r +plot$data -- string key into state$figures (e.g., "plots/1.png") +plot$name -- identifier +plot$title -- display title +plot$width -- pixel width +plot$height -- pixel height +plot$status -- "complete" +``` + +### In `results$state$figures` +Actual plot objects stored here, keyed by the `data` path: +```r +results$state$figures[["plots/1.png"]]$obj -- the plot object +results$state$figures[["plots/1.png"]]$width +results$state$figures[["plots/1.png"]]$height +``` + +### Plot Object Types +- **`jaspGraphsPlot`** (R6 class) -- composite plot with `$subplots` list of ggplot objects +- **Plain `ggplot`** -- single ggplot object (no subplots) + +### Retrieving Plot for Testing +```r +plotName <- results[["results"]][["plotElement"]][["data"]] +testPlot <- results[["state"]][["figures"]][[plotName]][["obj"]] +jaspTools::expect_equal_plots(testPlot, "snapshot-name") +``` + +## 6) State Object (`results$state`) + +- `state$figures` -- named list of plot objects (keyed by "plots/N.png") +- `state$other` -- named list of cached R objects (keyed by "state_N") + - Used by `createJaspState()` for caching expensive computations between output elements + +## 7) Testing Utilities + +### `expect_equal_tables(table_data, reference_list)` +1. Takes `table$data` (list of row-lists) +2. Flattens via `unname(unlist(rows))` -- row-by-row, fields in alphabetical order within each row +3. Converts numeric strings back to numbers via `charVec2MixedList` +4. Replaces unicode characters with `` placeholder +5. Compares element-by-element against flat reference list + +**Reference list format:** Single flat `list(...)` with all values row-by-row, fields alphabetically sorted: +```r +# For a table with fields: df, est, name, pval (alphabetical) +# Row 1: df=9, est=-0.69, name="Intercept", pval=0.50 +# Row 2: df=9, est=0.29, name="Slope", pval=0.01 +jaspTools::expect_equal_tables(table_data, + list(9, -0.69, "Intercept", 0.50, # row 1 + 9, 0.29, "Slope", 0.01)) # row 2 +``` + +### `expect_equal_plots(plot_obj, snapshot_name)` +- If `jaspGraphsPlot`: splits into subplots, each compared via `vdiffr::expect_doppelganger` with name `"snapshot-name-subplot-N"` +- If plain `ggplot`: compared directly via `vdiffr::expect_doppelganger` +- SVG snapshots stored in `tests/testthat/_snaps/` + +## 8) Quick Reference: Navigating Results + +```r +# Run analysis +results <- jaspTools::runAnalysis("AnalysisName", dataset, options, view = FALSE) + +# Check status +results$status # "complete" or "fatalError" +results$results$errorMessage # if fatalError + +# Get table data (for expect_equal_tables) +results[["results"]][["containerName"]][["collection"]][["containerName_tableName"]][["data"]] + +# Get plot object (for expect_equal_plots) +plotKey <- results[["results"]][["plotName"]][["data"]] +plotObj <- results[["state"]][["figures"]][[plotKey]][["obj"]] + +# Inspect table schema +table$schema$fields # list of {name, title, type, format, overTitle} + +# Map entire tree (debug helper) +mapResults <- function(x, depth = 0) { + indent <- paste(rep(" ", depth), collapse = "") + if (is.list(x) && !is.null(x$collection)) { + cat(sprintf("%s[container] %s: '%s'\n", indent, x$name, x$title)) + for (child in x$collection) mapResults(child, depth + 1) + } else if (is.list(x) && !is.null(x$schema)) { + cat(sprintf("%s[table] %s: '%s' (%d rows x %d cols)\n", + indent, x$name, x$title, length(x$data), length(x$schema$fields))) + } else if (is.list(x) && !is.null(x$data) && is.character(x$data)) { + cat(sprintf("%s[plot] %s: '%s'\n", indent, x$name, x$title)) + } +} +for (item in results$results[setdiff(names(results$results), c(".meta", "name"))]) { + mapResults(item) +} +``` diff --git a/.codex/rules/jasp-plots.md b/.codex/rules/jasp-plots.md new file mode 100644 index 0000000..0163345 --- /dev/null +++ b/.codex/rules/jasp-plots.md @@ -0,0 +1,131 @@ +# JASP Plot Building Patterns + +How to create and configure plots in jaspResults. + +For dependency mechanics see [jasp-dependency-management.md](jasp-dependency-management.md). +For testing plots see [testing-instructions.md](testing-instructions.md) (`expect_equal_plots`). + +--- + +## 1) Simple Plot + +```r +.myPlot <- function(jaspResults, options) { + + if (!is.null(jaspResults[["myPlot"]])) + return() + + fit <- .extractFit(jaspResults, options) + if (is.null(fit) || jaspBase::isTryError(fit[[1]])) + return() + + myPlot <- createJaspPlot( + title = gettext("My Plot"), + width = 400, + height = 320 + ) + myPlot$position <- 5 + myPlot$dependOn(c(.baseDependencies, "plotSpecificOption")) + jaspResults[["myPlot"]] <- myPlot + + # Build ggplot + plotObj <- ggplot2::ggplot(...) + ... + + # Add JASP theme and (plot frame b = bottom, r = right, t = top, l = left) + plotObj <- plotObj + + jaspGraphs::geom_rangeframe(sides = "bl") + + jaspGraphs::themeJaspRaw() + + myPlot$plotObject <- plotObj +} +``` + +--- + +## 2) Plot with Error Handling + +Wrap plot construction in `try()` and display the error on the plot element: + +```r +plotOut <- try(.makePlot(fit, options)) + +if (inherits(plotOut, "try-error")) { + myPlot <- createJaspPlot(title = gettext("My Plot")) + myPlot$dependOn(dependencies) + myPlot$setError(plotOut) + jaspResults[["myPlot"]] <- myPlot + return() +} + +myPlot <- createJaspPlot(title = gettext("My Plot"), width = w, height = h) +myPlot$plotObject <- plotOut +jaspResults[["myPlot"]] <- myPlot +``` + +--- + +## 3) Composite Plot (jaspGraphsPlot) + +For plots with multiple panels (e.g., a left annotation panel + right data panel): + +```r +plotObj <- jaspGraphs:::jaspGraphsPlot$new( + subplots = list(leftPanel, rightPanel), + layout = matrix(1:2, ncol = 2), + heights = 1, + widths = c(0.4, 0.6) +) +myPlot$plotObject <- plotObj +``` + +In tests, each subplot gets its own SVG snapshot: `"name-subplot-1"`, `"name-subplot-2"`. + +--- + +## 4) Per-Group Plot Pattern + +When a single fit produces a single plot, but multiple groups produce a container of plots: + +```r +if (options[["groupingVariable"]] == "") { + # Single plot, attach directly + plot <- .makePlotFun(fit[[1]], options) + plot$title <- gettext("My Plot") + plot$dependOn(dependencies) + jaspResults[["myPlot"]] <- plot + +} else { + # Container with one plot per group + container <- createJaspContainer() + container$title <- gettext("My Plot") + container$dependOn(dependencies) + jaspResults[["myPlot"]] <- container + + for (i in seq_along(fit)) { + container[[names(fit)[i]]] <- .makePlotFun(fit[[i]], options) + container[[names(fit)[i]]]$title <- gettextf("Group: %1$s", attr(fit[[i]], "group")) + container[[names(fit)[i]]]$position <- i + } +} +``` + +--- + +## 5) Separate-Plots-by-Variable Pattern + +When a variable creates multiple faceted plots: + +```r +if (length(options[["separatePlots"]]) > 0) { + container <- createJaspContainer() + for (i in seq_along(levels)) { + tempPlot <- createJaspPlot(title = levels[i], width = w, height = h) + tempPlot$position <- i + tempPlot$plotObject <- makePlot(data[data$facet == levels[i], ]) + container[[paste0("plot", i)]] <- tempPlot + } +} else { + plot <- createJaspPlot(width = w, height = h) + plot$plotObject <- makePlot(data) +} +``` diff --git a/.codex/rules/jasp-state-management.md b/.codex/rules/jasp-state-management.md new file mode 100644 index 0000000..7b3ea79 --- /dev/null +++ b/.codex/rules/jasp-state-management.md @@ -0,0 +1,252 @@ +# JASP State Management (createJaspState) + +How to cache expensive computations and track dynamic output state. + +For the reactive loop context see [jasp-module-architecture.md](jasp-module-architecture.md). +For dependency mechanics see [jasp-dependency-management.md](jasp-dependency-management.md). + +Note that you cannot test this by running analysis via `runAnalysis(..., view = FALSE)` because you only generate one state at a time +(with no initial elements - ask the human maintainer to validate the dependencies manually if you suspect an issue!). + +--- + +## 1) Why State Objects Exist + +Model fitting is expensive. Without caching, every option change (even toggling a checkbox for an unrelated table) would re-run the computation. State objects solve this by caching results that persist across R invocations as long as their dependencies hold. + +```r +.computeModel <- function(jaspResults, dataset, options) { + if (!is.null(jaspResults[["modelFit"]])) + return() # cached → skip + + fitState <- createJaspState() + fitState$dependOn(.modelDeps) # only model options + jaspResults[["modelFit"]] <- fitState + + result <- try(expensiveFit(dataset, options)) + fitState$object <- result # cache +} +``` + +Now when the user toggles "Show CI" (a table option, not a model option), `jaspResults[["modelFit"]]` survives. Only when a model option changes does the fit get invalidated and recomputed. + +--- + +## 2) The $object Property + +`createJaspState()` stores arbitrary R objects via `$object`: + +```r +# Store anything: model fits, lists, data.frames +jaspResults[["modelFit"]]$object <- list(model = fitResult, residuals = resid) + +# Retrieve in another builder function +cached <- jaspResults[["modelFit"]]$object +if (is.null(cached)) return() # not yet computed +model <- cached$model +``` + +--- + +## 3) State vs Output Elements + +| | State | Table/Plot/Html | +|---|---|---| +| Visible to user | No | Yes | +| Has `$object` | Yes | No (use `$setData()`, `$plotObject`) | +| Purpose | Cache computations | Display results | +| `$dependOn()` | Yes | Yes | +| Can nest in container | Yes | Yes | + +--- + +## 4) Pattern: Model Fit Caching + +The most common pattern -- fit a model once, reuse across multiple tables and plots: + +```r +.computeModel <- function(jaspResults, dataset, options) { + if (!is.null(jaspResults[["modelFit"]])) + return() + + fitState <- createJaspState() + fitState$dependOn(.modelDeps) + jaspResults[["modelFit"]] <- fitState + + fit <- try(myPackage::fitModel( + formula = .buildFormula(options), + data = dataset + )) + + fitState$object <- fit +} + +# Used by multiple builders: +.extractFit <- function(jaspResults) { + cached <- jaspResults[["modelFit"]]$object + if (is.null(cached)) return(NULL) + return(cached) +} +``` + +--- + +## 5) Pattern: Multiple Fits (Per Group / Per Variable) + +When the analysis computes separate fits for groups or variables, store them as a named list: + +```r +.computeModel <- function(jaspResults, dataset, options) { + if (!is.null(jaspResults[["modelFit"]])) + return() + + fitState <- createJaspState() + fitState$dependOn(.modelDeps) + jaspResults[["modelFit"]] <- fitState + + results <- list() + + # Overall fit + results[["overall"]] <- try(fitFun(dataset, options)) + + # Per-group fits (if grouping variable selected) + if (options[["groupingVariable"]] != "") { + groups <- unique(dataset[[options[["groupingVariable"]]]]) + for (g in groups) { + subData <- dataset[dataset[[options[["groupingVariable"]]]] == g, ] + fit <- try(fitFun(subData, options)) + attr(fit, "group") <- as.character(g) # preserve metadata even on error + results[[paste0("group_", g)]] <- fit + } + } + + fitState$object <- results +} +``` + +**Key conventions:** +- Use `attr(fit, "group")` to tag each fit with its group label (survives `try()` errors) +- Extractors can filter: include/exclude overall, handle errors per group +- Row builders iterate over fits via `lapply()`, returning skeleton data.frames on error + +### Extractor with filtering + +```r +.extractFit <- function(jaspResults, options) { + results <- jaspResults[["modelFit"]]$object + if (is.null(results)) return(NULL) + + # Optionally exclude overall fit + if (options[["groupingVariable"]] != "" && !options[["includeOverall"]]) + results <- results[names(results) != "overall"] + + return(results) +} +``` + +--- + +## 6) Pattern: Shared Computation Cache + +When multiple output elements (table + plot) need the same intermediate result: + +```r +.computeDiagnostics <- function(jaspResults, options) { + if (!is.null(jaspResults[["diagnosticsCache"]])) + return(jaspResults[["diagnosticsCache"]]$object) + + state <- createJaspState() + state$dependOn(.diagnosticsDeps) + jaspResults[["diagnosticsCache"]] <- state + + results <- expensiveComputation(...) + state$object <- results + return(results) +} +``` + +Both `.diagnosticsTable()` and `.diagnosticsPlot()` call `.computeDiagnostics()` -- the second call returns the cached result immediately. + +--- + +## 7) Pattern: Metadata State for Dynamic Containers + +When the set of output children depends on user-selected variables, track what's currently rendered: + +```r +.buildVariableOutputs <- function(jaspResults, options) { + + container <- .extractContainer(jaspResults) + + # Get or create metadata state + if (!is.null(container[["metaData"]])) { + meta <- container[["metaData"]]$object + } else { + metaState <- createJaspState() + metaState$dependOn(c("selectedVariables")) + container[["metaData"]] <- metaState + meta <- list(existing = character(0)) + } + + selected <- options[["selectedVariables"]] + existing <- meta$existing + + # Remove deselected + for (v in setdiff(existing, selected)) + container[[v]] <- NULL + + # Add new + for (v in setdiff(selected, existing)) { + child <- createJaspContainer(title = v) + child$position <- which(selected == v) + container[[v]] <- child + .buildTableForVariable(child, jaspResults, options, v) + } + + # Update tracking + container[["metaData"]]$object <- list(existing = selected) +} +``` + +This avoids rebuilding the entire container when the user adds or removes a single variable. + +--- + +## 8) Pattern: Dataset Update Sentinel + +When an expensive fit should NOT be re-run for visualization-only option changes, but auxiliary data attached to the fit needs updating: + +```r +.updateFitData <- function(jaspResults, dataset, options) { + if (is.null(jaspResults[["modelFit"]])) + return() + if (!is.null(jaspResults[["fitDataUpdate"]])) + return() + + # Create sentinel with narrow deps + sentinel <- createJaspState() + sentinel$dependOn(.plottingVariableDeps) + jaspResults[["fitDataUpdate"]] <- sentinel + + # Update auxiliary data on the existing (cached) fit + fit <- jaspResults[["modelFit"]]$object + fit$plotData <- .prepPlotData(fit, dataset, options) + jaspResults[["modelFit"]]$object <- fit + + sentinel$object <- TRUE # mark as done +} +``` + +When a plotting variable changes: sentinel is NULLed, data is re-attached. The model fit itself survives. + +--- + +## 9) Common Pitfalls + +**Forgetting to store:** Creating a state but never assigning `$object` -- extractors see NULL. + +**Circular extraction:** An extractor that calls the compute function which calls the extractor. Use the `if (!is.null(...)) return()` guard pattern consistently. + +**Overwriting state from extractors:** Extractors should be read-only. Only the compute function should write to `$object`. + +**State without dependencies:** A state with no `$dependOn()` is never invalidated -- it persists forever with potentially stale data. diff --git a/.codex/rules/jasp-tables.md b/.codex/rules/jasp-tables.md new file mode 100644 index 0000000..523cd5b --- /dev/null +++ b/.codex/rules/jasp-tables.md @@ -0,0 +1,197 @@ +# JASP Table Building Patterns + +How to create, configure, and populate tables in jaspResults. + +For dependency mechanics see [jasp-dependency-management.md](jasp-dependency-management.md). +For state/caching see [jasp-state-management.md](jasp-state-management.md). +For containers and error handling see [jasp-containers-and-errors.md](jasp-containers-and-errors.md). + +--- + +## 1) Complete Table Lifecycle + +```r +.myTable <- function(jaspResults, options) { + + container <- .myExtractContainer(jaspResults) + + # 1. SKIP if already created (idempotency) + if (!is.null(container[["myTable"]])) + return() + + fit <- .extractFit(jaspResults, options) + + # 2. CREATE table and attach to parent BEFORE filling data + myTable <- createJaspTable(gettext("My Table Title")) + myTable$position <- 1 + myTable$dependOn(c("optionA", "optionB")) + container[["myTable"]] <- myTable + + # 3. DEFINE columns + myTable$addColumnInfo(name = "term", type = "string", title = "") + myTable$addColumnInfo(name = "est", type = "number", title = gettext("Estimate")) + myTable$addColumnInfo(name = "se", type = "number", title = gettext("Standard Error")) + myTable$addColumnInfo(name = "pval", type = "pvalue", title = gettext("p")) + + # 4. EARLY RETURN on error (table shows as empty with error) + if (is.null(fit)) + return() + if (length(fit) == 1 && jaspBase::isTryError(fit[[1]])) { + myTable$setError(.cleanErrorMessage(fit[[1]])) + return() + } + + # 5. BUILD row data (list of data.frames → rbind) + rows <- do.call(rbind, lapply(fit, .myRowBuilder, options = options)) + + # 6. ADD footnotes + myTable$addFootnote(gettext("Some methodological note.")) + + # 7. SET data + myTable$setData(rows) +} +``` + +**Key**: Always attach the table to jaspResults (step 2) **before** checking errors (step 4). This ensures the empty table with error message displays rather than nothing. See [jasp-containers-and-errors.md](jasp-containers-and-errors.md) for the create-then-error pattern. + +--- + +## 2) Column Types + +| Type | Use for | Format examples | +|------|---------|-----------------| +| `"string"` | Labels, names, formatted test stats | -- | +| `"number"` | Numeric values | `"sf:4;dp:3"` (4 sig figs, 3 decimal places) | +| `"integer"` | Counts, df | -- | +| `"pvalue"` | p-values | `"dp:3;p:.001"` (3 dp, threshold at .001) | + +--- + +## 3) Column Modifiers + +```r +# Grouped column header (e.g., "95% CI" spanning Lower/Upper) +table$addColumnInfo(name = "lCi", type = "number", title = gettext("Lower"), + overtitle = gettextf("%s%% CI", 100 * options[["ciLevel"]])) + +# Show only explicitly added columns (hide data columns not in schema) +table$showSpecifiedColumnsOnly <- TRUE +``` + +--- + +## 4) DRY Pattern: Reusable Column Helpers + +When multiple tables share the same column groups (e.g., CI columns, SE columns, test statistics), factor out repeated `addColumnInfo()` calls into shared helper functions. For example, a helper that conditionally adds a CI lower/upper pair with a dynamic overtitle avoids duplicating those 3-4 lines across every table builder. + +Apply the same pattern for any column group that appears in more than one table — each helper takes the table and relevant options, and adds the columns conditionally. + +--- + +## 5) Parameterized Tables + +When the same table structure serves multiple purposes, parametrize the builder: + +```r +.myTable <- function(jaspResults, options, parameter = "main") { + + container <- .extractContainer(jaspResults) + tableKey <- paste0(parameter, "Table") + + if (!is.null(container[[tableKey]])) + return() + + table <- createJaspTable(switch(parameter, + main = gettext("Main Results"), + summary = gettext("Summary Results") + )) + table$position <- switch(parameter, main = 1, summary = 2) + container[[tableKey]] <- table + # ... columns and data +} +``` + +--- + +## 6) Row Builder Pattern + +Each row builder takes a **single fit** and returns a **data.frame** (one or more rows): + +```r +.myRowBuilder <- function(fit, options) { + + # Handle failed fits gracefully (return skeleton with NAs) + if (jaspBase::isTryError(fit)) { + return(data.frame( + term = gettext("My term"), + group = attr(fit, "group") + )) + } + + row <- data.frame( + term = gettext("My term"), + group = attr(fit, "group"), + est = fit$beta[1], + se = fit$se[1], + pval = fit$pval[1] + ) + + return(row) +} +``` + +**Key conventions:** +- Include `group = attr(fit, "group")` for per-group support +- On error, return data.frame with labels but missing numeric columns (renders as empty cells) +- Use `gettext()` / `gettextf()` for all user-visible strings + +--- + +## 7) DRY Pattern: Safe Data Aggregation + +When combining data.frames from multiple fits — especially when some fits may fail and return fewer columns — create a helper that: + +1. Filters out NULL/empty data.frames +2. Computes the union of all column names +3. Pads each data.frame with NA for missing columns +4. Calls `do.call(rbind, ...)` on the aligned data.frames + +This avoids `rbind()` failures when partial errors produce data.frames with heterogeneous columns. Apply the same helper pattern for ordering rows by grouping variable and simplifying output (e.g., dropping a grouping column when no groups are selected). + +--- + +## 8) Footnotes + +```r +# Simple footnote (appears at bottom) +table$addFootnote(gettext("Fixed effects tested using Knapp and Hartung adjustment.")) + +# Warning-style footnote +table$addFootnote(warningMsg, symbol = gettext("Warning:")) + +# Per-group error footnotes +for (i in which(sapply(fit, jaspBase::isTryError))) { + table$addFootnote( + gettextf("The model for group '%1$s' failed: %2$s", + attr(fit[[i]], "group"), .cleanError(fit[[i]])), + symbol = gettext("Error:") + ) +} + +# Cell-specific footnote +table$addFootnote(message, colNames = "est", rowNames = "rowLabel") +``` + +--- + +## 9) Error Display on Tables + +```r +# Error message replaces entire table content +table$setError(gettext("Feature not available for this model type.")) + +# Error from a try-error object +table$setError(.cleanErrorMessage(tryResult)) +``` + +See [jasp-containers-and-errors.md](jasp-containers-and-errors.md) for the full create-then-error and graceful degradation patterns. diff --git a/.codex/rules/qml-instructions.md b/.codex/rules/qml-instructions.md new file mode 100644 index 0000000..3a69f39 --- /dev/null +++ b/.codex/rules/qml-instructions.md @@ -0,0 +1,174 @@ +# JASP QML Instructions + +## 0) QML Syntax Validation + +**ALWAYS validate QML files after editing** using `qmllint` to catch syntax errors: + +```powershell +qmllint inst\qml\path\to\file.qml +``` + +- **Ignore import warnings**: Warnings about missing `JASP.Controls` and `JASP` modules are expected (qmllint lacks JASP's custom modules) +- **Focus on syntax errors**: Look for missing braces `{}`, brackets `[]`, parentheses `()`, semicolons, or malformed property assignments +- **Exit code matters**: Non-zero exit with syntax errors blocks parsing; zero exit means parseable (even with import warnings) +- **Run before committing**: Catch structural issues (extra/missing braces) that break QML parsing + +Example of ignorable warnings: +``` +Warning: Failed to import JASP.Controls [import] +Warning: IntegerField was not found [import] +``` + +Example of critical errors: +``` +Error: Expected token `}' [syntax] +``` + +## 1) Core Basics + +- **Imports:** + ```qml + import QtQuick + import QtQuick.Layouts + import JASP.Controls + import JASP + ``` + +- **Form as root:** Every analysis UI is a `Form { ... }` containing controls, usually a `VariablesForm` block and option controls. +- **Binding & IDs:** Prefer *property bindings* (reactive JS expressions) over imperative changes; reference other items via `id:` and bind (`enabled: show.checked || useAlt.checked`). +- **Stable storage names:** The `name:` of a control maps to stored options in JASP files; **avoid renaming**. If you must rename a released option, handle migrations in `Upgrades.qml`; for unreleased analyses, keep only the current name. +- **Exact backend API:** Every `name:` is the exact R option key. When renaming an option, update all R `options[["..."]]` reads and `$dependOn()` vectors to the current name; do not rely on R aliases or normalization for unreleased analyses. +- **QML/R option contract:** Every option read by R must be defined in the main QML or an imported component loaded by the GUI. Do not inline reusable components solely because `analysisOptions()` cannot discover them; use source-aware audits/explicit test options and fix real name mismatches. +- **Translation & docs:** + - Wrap **all user-visible strings** in `qsTr("Text")`. + - Populate `info:` with a short, user-facing description (also wrapped in `qsTr`) to feed module help. +- **Variables workflow:** Place variable pickers inside a `VariablesForm`; connect lists with `source:` (can read all data columns, other lists, levels, or R sources). + +## 2) Input Validation + +Prefer **declarative validation** via built-in field properties: + +- **Numeric fields** (`DoubleField`, `IntegerField`): set `min`, `max`, and `inclusive` (e.g., `MinMax`), `decimals` (for doubles), and allow negatives only when needed. Use `fieldWidth` for compact UI. +- **Percent & CI** (`PercentField`, `CIField`): sensible defaults (e.g., 95), `afterLabel` defaults to `"%"`. +- **Slider:** set `min`, `max`, `decimals`; prefer horizontal sliders unless space constrained. +- **FormulaField:** accepts R-style expressions; constrain with `min`, `max`, `inclusive`; use `multiple: true` only when arrays are intended. Read via `realValue` / `realValues`. +- **TableView:** for mixed types, define validators and override `getValidator(col,row)`; optionally specify `itemTypePerRow/Column`. +- **Variables lists:** enforce data types via `allowedColumns: ["scale"|"ordinal"|"nominal"]` and `singleVariable: true` where appropriate. + +## 3) Main Custom Components + +### General input +- **CheckBox** — `name`, `label`, `checked`, `childrenOnSameRow`, `columns` (nested controls auto-enable/disable). +- **RadioButtonGroup / RadioButton** — group has `name`, `title`, `radioButtonsOnSameRow`, `columns`; each button has `value`, `label`, `checked`; can contain nested controls per choice. +- **DropDown** — `name`, `label`, `values` (array or `{label, value}`), or `source`; selection via `startValue` / `currentValue`; `addEmptyValue`, `placeHolderText`. +- **Slider** — `name`, `label`, `value`, `min`, `max`, `decimals`. +- **DoubleField / IntegerField** — `label`, `defaultValue`, `min`, `max`, `inclusive`, (`decimals` for DoubleField). +- **PercentField / CIField** — percent-specific shorthand; defaults appropriate for CIs. +- **TextField** — `defaultValue` or `placeholderText` (mutually exclusive), `afterLabel`, `fieldWidth`. +- **FormulaField** — adds `realValue`, `min/max`, `inclusive`, `multiple`, `realValues`. +- **TextArea** — `title`, `text`, `textType` (e.g., R code / JAGS / Lavaan / Model / Source), `separator(s)`, `applyScriptInfo` (submit with **Ctrl+Enter**). + +### Variable specification +- **AvailableVariablesList** — `name`, `label`, **rich `source`** (other lists, levels, filters, `rSource`, combinations), or `values`; `width`, `count` (read-only). +- **AssignedVariablesList** — `name`, `label`, `allowedColumns`, `singleVariable`, `maxRows`, `listViewType` (e.g., `Interaction`), optional `rowComponent` (+ `rowComponentTitle`), `optionKey`, `count`. +- **FactorLevelList** — define RM factors/levels: `factorName`, `levelName`, `minFactors`, `minLevels`, `width`, `height`. Often paired with an `AssignedVariablesList` of type `MeasuresCells`. + +### Complex composition +- **ComponentsList** — templated rows of controls from a `source` or `values`; `titles`, `rowComponent`, manual rows via `addItemManually`, bounds via `minimumItems` / `maximumItems`, collected under `optionKey`. +- **TabView** — `ComponentsList` rendered as tabs. +- **InputListView** — user adds rows via an input field; `title`, `placeHolder`, `defaultValues`, `minRows`, `inputComponent` (Text/Double/Integer), optional `rowComponent`, `optionKey`. +- **TableView** — `name`, `modelType` (`MultinomialChi2Model`, `JAGSDataInputModel`, `FilteredDataEntryModel`, `CustomContrasts`), `itemType` or per-row/column types, `source`; may override `getColHeaderText`, `getRowHeaderText`, `getDefaultValue`, `getValidator`. + +### Grouping & structure +- **Group** — logical block with `title`, `columns`. Nest options inside. +- **Section** — collapsible panel for advanced options; `title`, `columns`. Use for lower-priority / expert settings. + +## 4) Style & UX Conventions + +- **Titles & labels:** Title Case for section/group titles; concise labels; every visible string uses `qsTr()`. The `name` is always the title transformed into camelCase. Options within groups inherit their names as a prefix. +- **Consistency:** Prefer the provided JASP controls over ad-hoc QML; nest subordinate options inside the control that enables them (e.g., a `CheckBox` containing its dependent fields). +- **Two-column rhythm:** Let the grid flow naturally; use `rowSpan/columnSpan` to avoid awkward gaps; avoid long single-column scrollers. +- **Variables first:** Place `VariablesForm` at the top; align list widths; restrict types with `allowedColumns`. +- **Defaults & placeholders:** Prefer meaningful `defaultValue`; use `placeholderText` only when input is optional. Don't set both. +- **Dropdowns:** Use `{label, value}` pairs when R-side value differs; add an explicit empty choice with `addEmptyValue` if "no selection" is valid. Preserve dynamic `DropDown.values` when they express the intended GUI; use `enabledOptions` only for intended disabled choices, not to expose tooling defaults. +- **Advanced options:** Tuck rare/expert settings into a `Section` titled "Advanced Options". +- **Docs:** Fill `info:` succinctly for every major control. +- **Spacing:** Always use tabs for spacing. Each argument on a new line. (See examples below.) + +## 5) Quick Patterns + +- **Enable dependent field(s):** + ```qml + CheckBox + { + id: show + name: "showX" + label: qsTr("Show X") + } + + DoubleField + { + name: "Alpha" + label: qsTr("Alpha") + defaultValue: 0.05 + min: 0 + max: 1 + decimals: 3 + enabled: show.checked + } + ``` + +- **Radio choice with per-choice inputs:** + ```qml + RadioButtonGroup + { + name: "crit" + title: qsTr("Criterion") + + RadioButton + { + value: "pValue" + label: qsTr("p-value") + checked: true + + DoubleField + { + name: "pValueValue" + label: "" + defaultValue: 0.05 + min: 0 + max: 1 + } + } + + RadioButton + { + ... + } + } + ``` + +- **Variables form (single DV):** + ```qml + VariablesForm + { + AvailableVariablesList + { + name: "availableVariables" + } + + AssignedVariablesList + { + name: "dependentVariable" + label: qsTr("Dependent Variable") + allowedColumns: ["scale"] + singleVariable: true + } + } + ``` + +## 6) When in doubt + +- Prefer built-in JASP controls. +- Keep `name:` stable; translate strings; validate inputs. +- Put rare/expert options in a `Section` and document via `info:`. diff --git a/.codex/rules/r-instructions.md b/.codex/rules/r-instructions.md new file mode 100644 index 0000000..5aea4e6 --- /dev/null +++ b/.codex/rules/r-instructions.md @@ -0,0 +1,116 @@ +# R Instructions + +## 1) Core Basics + +- **Main entry point (name matters):** + - The R function name **must match** the case-sensitive `"function"` field in `Description.qml`. + - Signature is always: + ```r + AnalysisName <- function(jaspResults, dataset, options) { ... } + ``` + - `jaspResults` is a container that stores all of the analysis output and byproducts (if they are supposed to be kept for later use). + - `dataset` is the loaded dataset in JASP + - `options` are the UI choices from QML; **do not rename** option keys (they're your API). + - Read GUI options directly with `options[["name"]]`; do not add R-side normalization, old-name aliases, compatibility maps, or backup defaults for missing QML options in unreleased work. + - For checkbox options, use `if (options[["flag"]])`, not `isTRUE(options[["flag"]])`; `isTRUE()` hides missing/disconnected options by treating them like `FALSE`. + - Keep option names in `$dependOn()` vectors synchronized with current QML `name:` values. + +- **Recommended structure (3 roles):** + 1) **Main function** orchestrates and wires output elements. + 2) **create* functions** declare output markup (tables/plots/text). + 3) **fill* (or compute*) functions** compute results and fill outputs. + +- **Dependencies (cache & reuse):** + Add `$dependOn()` to every output (table/plot/text/container/state) so JASP knows when to reuse or drop it. + Outputs nested within containers inherit all dependencies from the container. + +- **NEVER instantiate jaspResults C++ objects directly** (e.g., `jaspResultsClass$new()`, `create_cpp_jaspResults()`, `jaspBase:::initJaspResults()`). These require JASP Desktop C++ initialization unavailable in headless R sessions. They crash with `Rcpp::not_initialized` or `Expecting an external pointer`. Always use `jaspTools::runAnalysis()` or `agentTestAll()` which handle initialization internally. + +- **Errors:** + - Catch run-time errors with `try(...)` and report via `$setError()`. + - Wrap user-visible text with `gettext()` / `gettextf()` for translation. + +--- + +## 2) Input Validation + +Only validate the `dataset`. `options` input is validated in the QML automatically. +Do not compensate for missing ordinary GUI options in R. Defaults belong in QML controls; if an option is absent, fix the QML/R mapping instead of adding fallback code. The exceptions are targeted validation for arbitrary user text, such as `TextField` and `FormulaField` options. + +Common checks (prefix arguments with the check name): +```r +.hasErrors( + dataset, type = c("factorLevels", "observations", "variance", "infinity", "missingValues"), + factorLevels.target = options$variables, + factorLevels.amount = "< 1", + observations.target = options$variables, + observations.amount = "< 1" +) +``` +Other useful checks: +- `limits.min/max` (inclusive bounds), +- `varCovData.target/corFun` (positive-definiteness), +- `modelInteractions` (ensure lower-order terms exist). + +--- + +## 3) Output Components + +For detailed patterns, examples, and lifecycle guides: + +- Tables: see [jasp-tables.md](jasp-tables.md) +- Plots: see [jasp-plots.md](jasp-plots.md) +- Containers, HTML, errors: see [jasp-containers-and-errors.md](jasp-containers-and-errors.md) +- State/caching: see [jasp-state-management.md](jasp-state-management.md) + +**Quick API reference:** + +| Element | Create | Key properties | +|---------|--------|----------------| +| Table | `createJaspTable(title)` | `$addColumnInfo()`, `$setData(df)`, `$addFootnote()`, `$setError()`, `$showSpecifiedColumnsOnly` | +| Plot | `createJaspPlot(title, width, height)` | `$plotObject <- ggplot(...)`, `$setError()` | +| HTML | `createJaspHtml(text)` | `$text`, `$dependOn()` | +| Container | `createJaspContainer(title)` | `$dependOn()` (propagates to children), nest freely | +| State | `createJaspState()` | `$object` (store/retrieve), `$dependOn()` | + +All elements support `$dependOn()`, `$position`, and `$addCitation()`. + +--- + +## 4) Style & Conventions + +- **Follow the project R style guide.** Keep functions short; prefer pure helpers; avoid global state; no I/O or printing in analyses. +- **Naming:** + - Helpers start with a dot, e.g., `.computeFoo()`, `.fillBarTable()`, `.plotBaz()`. + - Stable keys in `jaspResults[["..."]]` (don't rename them later). +- **Internationalization:** All visible text via `gettext()`/`gettextf()`. +- **Performance:** Read only needed columns; postpone decoding; reuse `createJaspState()` when multiple outputs share results. +- **Robustness:** Validate early; guard long loops with `if (!ready) return()`; wrap risky code in `try()` and call `$setError()`. +- **Reproducibility:** Set column formats explicitly in tables; document assumptions in footnotes/citations. +- **Assignment alignment:** +For related assignments allign them at the arrow `<-`, i.e., +``` +variableOne <- foo() +variableFive <- foo() +``` +and allign function arguments in the similar way for function whose call is too long to be on a single line: +``` +out <- foo( + argumentOne = variableOne, + argumentFive = variableFive, + ... +) + +--- + +## 5) Minimal main() template (copy/paste) + +```r +MyAnalysis <- function(jaspResults, dataset, options) { + + ready <- length(options[["variables"]]) > 0 + + .createMyTable(jaspResults, dataset, options, ready) + .createMyPlot(jaspResults, dataset, options, ready) +} +``` diff --git a/.codex/rules/testing-instructions.md b/.codex/rules/testing-instructions.md new file mode 100644 index 0000000..308742a --- /dev/null +++ b/.codex/rules/testing-instructions.md @@ -0,0 +1,174 @@ +# JASP Testing Instructions + +## 1) Test Framework + +This module uses the `jaspTools` testing framework. Tests are **critical** and must always pass before committing code. + +## 2) Running Tests + +Run via `btw_tool_run_r` in the persistent R session: + +**Agent-optimized** (preferred -- compact output, returns queryable result object): + +```r +# Full test suite -- returns rich S3 result object +x <- agentTestAll() + +# Specific analysis tests +x <- agentTestAnalysis("AnalysisName") +``` + +These return a `jaspAgentTestResults` object with fields: `$status`, `$summary`, `$failures`, `$warnings`, `$skips`, `$tests`, `$errorModules`, `$logFile`. + +**Human-oriented** (verbose output, for interactive use): + +```r +testAll() +testAnalysis("AnalysisName") +``` + +**MCP timeout for large modules:** `agentTestAll()` can take 180-300+ seconds. If `btw_tool_run_r` times out, fall back to Bash: + +```bash +Rscript --no-init-file -e ' + renv::load() + library(jaspTools) + setupJaspTools(pathJaspDesktop="/opt/jasp-desktop", installJaspModules=FALSE, installJaspCorePkgs=FALSE, quiet=TRUE, force=TRUE) + setPkgOption("module.dirs", ".") + setPkgOption("reinstall.modules", FALSE) + agentTestAll() +' +``` + +Do NOT retry via MCP after a timeout -- use the Bash fallback immediately. + +**Critical rules:** + +- Tests take 300+ seconds to complete +- **NEVER CANCEL** tests -- always let them run to completion +- Some deprecation warnings are expected and can be ignored +- ALL tests must pass before proceeding +- Some tests skip on certain platforms (e.g., Windows) -- this is expected +- Some stderr noise (ggplot messages, tryCatch errors) may leak through -- expected and minor + +## 3) Test File Structure + +Each test file in `tests/testthat/` corresponds to an R analysis file: + +- `test-penalizedmetaanalysis.R` -> `R/penalizedmetaanalysis.R` +- Test file name pattern: `test-.R` +- Analysis names for `agentTestAnalysis()` come from NAMESPACE exports (PascalCase) + +## 4) Writing Tests + +### Basic test structure + +```r +# 1. Set up analysis options +options <- jaspTools::analysisOptions("AnalysisName") +options$variables <- "contGamma" +options$descriptives <- TRUE + +# 2. Set seed for reproducibility +set.seed(1) + +# 3. Run the analysis +results <- jaspTools::runAnalysis("AnalysisName", "debug.csv", options, view = FALSE) + +# 4. Test tables +test_that("Table name matches", { + table <- results[["results"]][["tableName"]][["data"]] + jaspTools::expect_equal_tables(table, list(...expected values...)) +}) + +# 5. Test plots +test_that("Plot name matches", { + plotName <- results[["results"]][["containerName"]][["collection"]][["plotId"]][["data"]] + testPlot <- results[["state"]][["figures"]][[plotName]][["obj"]] + jaspTools::expect_equal_plots(testPlot, "plotname", dir = "AnalysisName") +}) +``` + +### Loading from .jasp example files + +```r +jaspFile <- testthat::test_path("..", "..", "examples", "Example Name.jasp") +opts <- jaspTools::analysisOptions(jaspFile) +dataset <- jaspTools::extractDatasetFromJASPFile(jaspFile) +encoded <- jaspTools:::encodeOptionsAndDataset(opts, dataset) +set.seed(1) +results <- jaspTools::runAnalysis("AnalysisName", encoded$dataset, encoded$options, encodedDataset = TRUE, view = FALSE) +``` + +### Key testing functions + +- `jaspTools::analysisOptions(name)` -- Get default options for an analysis +- `jaspTools::runAnalysis(name, dataset, options, view = FALSE)` -- Run analysis without generating HTML; inspect the returned R object +- `jaspTools::expect_equal_tables(actual, expected)` -- Compare table output +- `jaspTools::expect_equal_plots(plot, name, dir)` -- Compare plot output (snapshot-based) + +## 5) Test Data + +- `"debug.csv"` is a built-in jaspTools dataset containing most data types +- Use `set.seed()` before running analyses for reproducibility +- Example .jasp files in `examples/` provide pre-configured options and datasets + +## 6) Test Snapshots + +- Snapshots stored in `tests/testthat/_snaps/` +- **NEVER automatically accept snapshot changes** -- always notify user for manual inspection +- When a new snapshot is created, inform the user so they can verify it + +## 7) When to Update Tests + +### Always update tests when + +1. Adding new analysis outputs (tables, plots, text) +2. Modifying existing output structure or values +3. Adding new QML options that affect results +4. Changing analysis calculations + +### How to update test expectations + +1. Run tests and capture new output +2. Verify the new output is correct +3. Update expected values in test file +4. Re-run tests to confirm they pass + +## 8) Test Workflow + +### Before making code changes + +Run `agentTestAll()` via `btw_tool_run_r` to establish baseline -- all tests should pass. + +### After making code changes + +1. Run `devtools::load_all()` to hot-reload R changes +2. Run `agentTestAnalysis("AnalysisName")` for quick iteration on the affected analysis +3. Once the specific tests pass, run `agentTestAll()` to check for regressions + +### If tests fail + +1. Review the failure messages carefully +2. Check if failure is expected (due to your intentional changes) +3. If expected: update test expectations and notify user about snapshot changes +4. If unexpected: fix your code +5. Re-run tests until all pass + +## 9) Adding New Tests + +When adding a new analysis: + +1. Create test file: `tests/testthat/test-.R` +2. Set up options with all default values explicitly set +3. Test all output tables and plots +4. Test edge cases and error conditions +5. Use meaningful variable names and test data + +## 10) Best Practices + +- **One test per output element** -- separate `test_that()` blocks for each table/plot +- **Descriptive test names** -- clearly state what is being tested +- **Reproducible** -- always use `set.seed()` for analyses with randomness +- **Complete option coverage** -- test with various option combinations +- **Keep tests focused** -- each test should verify one specific aspect diff --git a/.codex/rules/translation-instructions.md b/.codex/rules/translation-instructions.md new file mode 100644 index 0000000..7a0a217 --- /dev/null +++ b/.codex/rules/translation-instructions.md @@ -0,0 +1,249 @@ +# Translation (i18n) Instructions + +## 1) Core Principle + +**ALL user-visible text must be wrapped for translation.** + +This module is translated into multiple languages via Weblate integration. + +## 2) R Code Translation + +### Use `gettext()` for static strings: +```r +# Single string +message <- gettext("Analysis complete") + +# Table titles +tab <- createJaspTable(title = gettext("Descriptive Statistics")) + +# Error messages +tab$setError(gettext("Insufficient observations")) +``` + +### Use `gettextf()` for dynamic strings: +```r +# Single placeholder +msg <- gettextf("Variable %s has insufficient data", varName) + +# Multiple placeholders - use numbered format for translators +msg <- gettextf("Number of factor levels is %1$s in %2$s", nLevels, varName) + +# Percentage signs must be doubled +label <- gettextf("%s%% CI for Mean Difference", 100 * alpha) +``` + +### Use `ngettext()` for plurals: +```r +msg <- ngettext(n, + "One observation removed", + "%d observations removed", + domain = "R-jaspEquivalenceTTests") +``` + +### Column overtitles with dynamic content: +```r +if (options$confidenceInterval) { + ciLabel <- gettextf("%s%% CI", 100 * options$confidenceIntervalLevel) + tab$addColumnInfo("lower", gettext("Lower"), overtitle = ciLabel) + tab$addColumnInfo("upper", gettext("Upper"), overtitle = ciLabel) +} +``` + +## 3) QML Translation + +### Wrap all visible strings with `qsTr()`: +```qml +CheckBox +{ + name: "descriptives" + label: qsTr("Descriptive statistics") + + CheckBox + { + name: "confidenceInterval" + label: qsTr("Confidence interval") + info: qsTr("Display confidence intervals for effect sizes") + } +} +``` + +### For groups and sections: +```qml +Group +{ + title: qsTr("Additional Statistics") + + CheckBox + { + label: qsTr("Effect size") + } +} + +Section +{ + title: qsTr("Advanced Options") + + DoubleField + { + label: qsTr("Prior scale") + } +} +``` + +### Radio buttons and dropdowns: +```qml +RadioButtonGroup +{ + name: "hypothesis" + title: qsTr("Alternative Hypothesis") + + RadioButton + { + value: "twoSided" + label: qsTr("Two-sided") + } + + RadioButton + { + value: "greater" + label: qsTr("Greater than") + } +} + +DropDown +{ + name: "effectSize" + label: qsTr("Effect Size") + values: [ + { label: qsTr("Cohen's d"), value: "cohen" }, + { label: qsTr("Glass' delta"), value: "glass" } + ] +} +``` + +## 4) Translation Rules + +### DO wrap for translation: +- ✅ Table/plot/container titles +- ✅ Column names and overtitles +- ✅ Error messages and warnings +- ✅ Footnotes and citations +- ✅ All QML labels, titles, and info text +- ✅ Help text and descriptions +- ✅ Button labels and tooltips + +### DON'T wrap for translation: +- ❌ Empty strings: `""` (NEVER mark for translation) +- ❌ Variable names (internal identifiers) +- ❌ Statistical symbols: `"β"`, `"p"`, `"t"`, `"df"` +- ❌ Mathematical expressions +- ❌ Code or syntax +- ❌ File paths + +### Format specifications: +```r +# CORRECT - use numbered placeholders for clarity +gettextf("Mean difference is %1$s with SE = %2$s", mean, se) + +# AVOID - unnamed placeholders are harder for translators +gettextf("Mean difference is %s with SE = %s", mean, se) +``` + +### Special characters: +```r +# Use UTF-8 escape sequences for non-ASCII +label <- gettext("Cram\u00E9r's V") # Cramér's V +symbol <- gettext("\u03B2") # β (beta) +``` + +### Percentage signs in format strings: +```r +# WRONG - single % will cause format error +label <- gettextf("%s% CI", 95) + +# CORRECT - double %% in format string +label <- gettextf("%s%% CI", 95) +``` + +## 5) Translation Workflow + +### Automated process: +1. Developers write code with `gettext()`/`gettextf()`/`qsTr()` +2. Translation extraction happens automatically +3. Weblate platform provides translation interface +4. Translators work on Weblate +5. Translation files synced back to repository automatically +6. `.github/workflows/translations.yml` handles automation + +### Translation files location: +``` +po/ # R translation files +inst/qml/translations/ # QML translation files (if exists) +``` + +### Manual updates (rare): +Usually handled automatically, but if needed: +```bash +# Update R translations (done by translation workflow) +# Don't manually edit .po files unless absolutely necessary +``` + +## 6) Testing Translations + +While we can't easily test all languages locally, ensure: +1. All user-visible strings are wrapped +2. Format strings use numbered placeholders +3. Percentage signs are doubled in format strings +4. No empty strings marked for translation +5. Context provided for ambiguous terms + +## 7) Common Mistakes to Avoid + +### ❌ WRONG: +```r +# Missing translation +tab <- createJaspTable(title = "Descriptive Statistics") + +# Empty string marked for translation +label <- gettext("") + +# Unnamed placeholders +msg <- gettextf("Found %s issues in %s", count, name) + +# Single % for percentage +label <- gettextf("%s% Confidence Interval", 95) +``` + +### ✅ CORRECT: +```r +# Proper translation +tab <- createJaspTable(title = gettext("Descriptive Statistics")) + +# No translation for empty string +label <- "" + +# Numbered placeholders for translators +msg <- gettextf("Found %1$s issues in %2$s", count, name) + +# Doubled %% for percentage +label <- gettextf("%s%% Confidence Interval", 95) +``` + +## 8) Translation Context + +For ambiguous terms, consider adding comments: +```r +# "Mean" as in average (not "mean" as in unkind) +columnTitle <- gettext("Mean") + +# "Scale" as in measurement scale (not fish scales) +fieldLabel <- qsTr("Scale variable") +``` + +## 9) Weblate Integration + +- Weblate repo: `jaspequivalencettests-qml` and `jaspequivalencettests-r` +- Automated workflow: `.github/workflows/translations.yml` +- Scheduled runs: Weekly on Saturday at 2:45 AM +- Manual trigger: `workflow_dispatch` available +- Translation updates automatically create commits/PRs diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..f5e4390 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,262 @@ +# JASP Module + +ALWAYS follow these instructions first and fallback to additional search and context gathering ONLY if the information in these instructions is incomplete or found to be in error. + +This is a JASP module. It contains QML user-facing interfaces and R backend computations. + +In all interactions and commit messages, be extremely concise and sacrifice grammar for the sake of concision. + +## Detailed Instructions + +For comprehensive guidance on specific topics, see: + +- **[Module Architecture](.github/instructions/jasp-module-architecture.instructions.md)** - **Start here.** QML-Desktop-R reactive loop, jaspResults persistence, options mapping, data flow +- **[Dependency Management](.github/instructions/jasp-dependency-management.instructions.md)** - $dependOn mechanics, inheritance, vectors, per-value deps, sentinel pattern +- **[State Management](.github/instructions/jasp-state-management.instructions.md)** - createJaspState caching, model fit patterns, metadata state, dynamic containers +- **[R Backend Development](.github/instructions/R.instructions.md)** - R function structure, validation, style conventions +- **[Tables](.github/instructions/jasp-tables.instructions.md)** - Table lifecycle, columns, rows, footnotes, error display +- **[Plots](.github/instructions/jasp-plots.instructions.md)** - Plot lifecycle, composite plots, subgroup/facet patterns +- **[Containers & Errors](.github/instructions/jasp-containers-and-errors.instructions.md)** - Container patterns, HTML output, error handling +- **[QML Interface Development](.github/instructions/inst.qml.instructions.md)** - QML controls, validation, bindings, and UI patterns +- **[Testing & Test Writing](.github/instructions/testing.instructions.md)** - Test framework, snapshots, and test workflow +- **[Translation (i18n)](.github/instructions/translation.instructions.md)** - gettext/gettextf/qsTr usage, formatting, plurals +- **[Output Structure](.github/instructions/jasp-output-structure.instructions.md)** - Reading/testing serialized output (containers, tables, plots, state) +- **[Debug Analysis](.github/instructions/debug-analysis.instructions.md)** - Debugging JASP analyses via saveRDS() state capture in MCP sessions + +## R Session via MCP + +This project uses the `btw` MCP server (`.claude/mcp-server.R`) to provide a persistent R session via `btw_tool_run_r`. The MCP server config (`.mcp.json`) is module-specific and NOT committed to git. + +**Session handoff:** The user sets up their R session (RStudio/Positron/radian), runs `btw::btw_mcp_session()`, and hands it over. Connect via `list_r_sessions` / `select_r_session`. All `btw_tool_run_r` calls then execute in the user's session with full access to loaded packages and objects. The following R packages are required for the mcp server: `btw`, `mcptools`. + +### Available MCP Tools + +These are MCP tools — invoke them directly as tool calls, not as R functions or shell commands: + +| Tool | Use for | +|------|---------| +| `list_r_sessions` | Discover available R sessions (call first) | +| `select_r_session` | Connect to a session from the list | +| `btw_tool_run_r` | Execute R code in persistent session (variables persist between calls) | +| `btw_tool_docs_help_page` | Look up R function documentation | +| `btw_tool_docs_package_news` | Check package changelogs | +| `btw_tool_docs_available_vignettes` | Find package vignettes | +| `btw_tool_env_describe_environment` | Inspect objects in the R session | +| `btw_tool_env_describe_data_frame` | Inspect data frame structure | +| `btw_tool_search_packages` | Search CRAN for packages | +| `btw_tool_session_platform_info` | Check R version and platform | +| `btw_tool_session_check_package_installed` | Verify package availability | + +**Use native tools** (Read, Edit, Write, Glob, Grep, Bash) for file editing, git operations, and file search -- they are faster than MCP equivalents. + +## Working Effectively + +### Session Setup (done by user) + +At the start of a session, check for a connected R session via `list_r_sessions`. If none is available, **prompt the user** to run in their interactive R console: + +```r +source(".claude/session_startup.R") +``` + +This restores dependencies, installs the module, configures jaspTools, and registers the session. Then connect via `list_r_sessions` / `select_r_session`. + +### Hot-Reload After Code Changes + +- **R code only changed:** `devtools::load_all()` via `btw_tool_run_r` +- **QML, dependencies, or imports changed:** `renv::install(".", prompt = FALSE)` + +### Running Tests + +Run via `btw_tool_run_r` in the persistent session: + +**Agent-optimized** (preferred -- compact output, returns queryable result object): + +```r +# Full test suite -- returns rich S3 result object +x <- agentTestAll() + +# Specific analysis tests +x <- agentTestAnalysis("AnalysisName") +``` + +These return a `jaspAgentTestResults` object. Console output is a compact one-line summary: +``` +== Test Results == FAIL: 0 | WARN: 0 | SKIP: 2 | PASS: 72 | Time: 3.6s +``` + +Query the result object directly: +```r +x$status # 0 = all passed, 1 = failures +x$summary # list(fail, warn, skip, pass, time) +x$failures # data.frame: module | file | test | message +x$warnings # data.frame: module | file | test | message +x$skips # data.frame: module | file | test | reason +x$tests # data.frame: all tests with module | file | context | test | passed | failed | ... +x$errorModules # named character vector of module-level errors +x$logFile # path to detailed JSON log (with backtraces) +``` + +**Human-oriented** (verbose output, for interactive use): +```r +testAll() +testAnalysis("AnalysisName") +``` + +**Rules:** +- Tests take 300+ seconds to complete -- **NEVER CANCEL** +- Run `agentTestAll()` at session start to verify baseline, and after all fixes +- Use `agentTestAnalysis("Name")` for quick iteration on specific analyses +- Analysis names are PascalCase exports from NAMESPACE +- Some tests skip on certain platforms (e.g., Windows) -- expected +- Some stderr noise (ggplot messages, tryCatch errors) may leak through -- expected and minor + +**See [testing.instructions.md](.github/instructions/testing.instructions.md) for detailed test writing guidelines, snapshots, and workflows.** + +### Running a Specific Analysis + +**With built-in debug dataset:** +```r +options <- jaspTools::analysisOptions("AnalysisName") +options$someOption <- value +set.seed(1) +results <- jaspTools::runAnalysis("AnalysisName", "debug.csv", options, view = FALSE) +``` + +**From a .jasp example file:** +```r +jaspFile <- file.path("examples", "Example Name.jasp") +opts <- jaspTools::analysisOptions(jaspFile) +dataset <- jaspTools::extractDatasetFromJASPFile(jaspFile) +encoded <- jaspTools:::encodeOptionsAndDataset(opts, dataset) +set.seed(1) +results <- jaspTools::runAnalysis("AnalysisName", encoded$dataset, encoded$options, encodedDataset = TRUE, view = FALSE) +``` + +The encoding step is required because JASP internally encodes variable names and options to resolve ambiguities (e.g., same variable used with different types). + +**From a user-provided .jasp file:** Use the same pattern above. This is the primary way to reproduce bugs reported by users. + +### Inspecting Results + +Always set `view = FALSE` when running `runAnalysis()` manually. This avoids HTML generation; inspect the returned R object instead. + +After `runAnalysis()`, check: +- `results$status` -- `"complete"` or `"fatalError"` +- `results$results` -- nested list of output containers, tables, plots +- `results$results$errorMessage` -- if status is fatalError + +### Finding Analysis Names + +1. Check roxygen documentation in R files (if available) +2. Parse `NAMESPACE` for `export()` directives + +### Test Snapshots + +- Snapshots stored in `tests/testthat/_snaps/` +- **NEVER automatically accept snapshot changes** -- always notify user for manual inspection +- When a snapshot is newly created, inform the user + +### Repository Structure +``` +/ +├── R/ # Backend R analysis functions +├── inst/ +│ ├── qml/ # QML interface definitions +│ ├── Descriptions/ # Analysis descriptions (Description.qml) +│ ├── help/ # Markdown help files +│ └── Upgrades.qml # Version upgrade mappings +├── examples/ # Example .jasp files for testing +├── tests/testthat/ # Unit tests using jaspTools +├── .github/workflows/ # CI/CD automation +├── DESCRIPTION # R package metadata +├── NAMESPACE # Exported analysis names +└── renv.lock # R dependency lockfile +``` + +### Key Files to Check After Changes +- Always check corresponding test file in `tests/testthat/` when modifying R functions +- Update `inst/Upgrades.qml` when renaming QML options to maintain backward compatibility + +## Development Rules + +### Dependencies +- Avoid new dependencies -- re-implement simple functions instead of importing a whole package +- If a new dependency is truly needed, add it to DESCRIPTION and update renv.lock + +### QML Interface Rules +- QML interfaces in `inst/qml/` define user-facing options passed to R functions +- Each analysis links: `inst/Description.qml/` -> `inst/qml/` -> `R/` functions +- QML elements use `name` (camelCase internal) and `title`/`label` (user-facing) +- Document QML elements using `info` property for help generation +- Use existing QML files as examples for structure and style +- Add default values to unit tests when adding new QML options + +**See [inst.qml.instructions.md](.github/instructions/inst.qml.instructions.md) for comprehensive QML controls reference, validation patterns, and UI conventions.** + +### R Backend Rules +- R functions in `R/` directory called by analyses in `inst/Descriptions/` +- Use camelCase for all function and variable names +- NEVER use `library()` or `require()` - use `package::function()` syntax +- Access `options` list via `options[["name"]]` notation to avoid partial matching +- Follow CRAN guidelines for code structure and documentation + +**See [R.instructions.md](.github/instructions/R.instructions.md) for complete R function structure, jaspResults API, output components (tables/plots/containers/state), and coding conventions.** + +### Input Validation and Error Handling +- **TARGETED VALIDATION ONLY**: Since `options` are validated in the GUI, R functions should NOT check user input validity except for specific cases +- **VALIDATE ONLY**: `dataset` object (data.frame from GUI), `TextField` options, and `FormulaField` options (arbitrary text input) +- Use `gettext()` and `gettextf()` for all user-visible messages (internationalization) +- For `dataset` validation, check: missing values, infinity, negative values, insufficient observations, factor levels, variance +- Example: `.hasErrors(dataset, type = c('observations', 'variance', 'infinity'), all.target = options$variables, observations.amount = '< 3', exitAnalysisIfErrors = TRUE)` +- Validate dataset assumptions automatically when required for analysis validity +- Use footnotes for assumption violations that affect specific cells/values +- Place critical errors that invalidate entire analysis over the results table + +### Error Message Guidelines +- Write clear, actionable error messages that prevent user confusion +- Use `gettextf()` with placeholders for dynamic content: `gettextf("Number of factor levels is %1$s in %2$s", levels, variable)` +- For multiple arguments, use `%1$s`, `%2$s` format for translator clarity +- Use `ngettext()` for singular/plural forms +- Never mark empty strings for translation +- Use UTF-8 encoding for non-ASCII characters: `\u03B2` for beta +- Double `%` characters in format strings: `gettextf("%s%% CI for Mean")` + +**See [translation.instructions.md](.github/instructions/translation.instructions.md) for comprehensive i18n guidelines including QML qsTr(), R gettext/gettextf/ngettext, formatting rules, and Weblate workflow.** + +## CI/CD Pipeline +- GitHub Actions in `.github/workflows/unittests.yml` runs on every push +- Triggers on changes to R, test, or package files +- Uses jasp-stats/jasp-actions reusable workflow + +## Git Workflow + +- **ALWAYS work on feature branches** -- never commit directly to `master` +- **NEVER push/create PRs/merge without explicit human approval** +- Commit locally freely, but wait for approval before pushing to remote + +## Common Tasks + +### Adding New Analysis + +1. Create R function in `R/` directory following camelCase naming +2. Add QML interface in `inst/qml/` +3. Define analysis in `inst/Description.qml` +4. Add unit tests in `tests/testthat/` +5. Run `agentTestAll()` to validate (300+ seconds, NEVER CANCEL) + +### Modifying Existing Analysis + +1. Update R function maintaining existing interface +2. Update QML if adding/changing options +3. Update unit tests and expected results +4. Add upgrade mapping to `inst/Upgrades.qml` if renaming options +5. Run tests: `agentTestAll()` (NEVER CANCEL, 300+ seconds) + +### Detailed Development Process +- **Step 1**: Create main analysis function with `jaspResults`, `dataset`, `options` arguments +- **Step 2**: **CRITICAL** - Use `.quitAnalysis()` for `dataset`, `TextField`, `FormulaField` validation only +- **Step 3**: Create output tables/plots with proper dependencies, citations, column specs +- 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 diff --git a/.github/instructions/R.instructions.md b/.github/instructions/R.instructions.md new file mode 100644 index 0000000..f83315d --- /dev/null +++ b/.github/instructions/R.instructions.md @@ -0,0 +1,189 @@ +--- +applyTo: "**/R/*.R" +description: "R function structure, validation, jaspResults API, output components, style conventions" +--- + +# R Instructions + +## 1) Core Basics + +- **Main entry point (name matters):** + - The R function name **must match** the case-sensitive `"function"` field in `Description.qml`. + - Signature is always: + ```r + AnalysisName <- function(jaspResults, dataset, options) { ... } + ``` + - `jaspResults` is a container that stores all of the analysis output and byproducts (if they are supposed to be kept for later use). + - `dataset` is the loaded dataset in JASP + - `options` are the UI choices from QML; **do not rename** option keys (they’re your API). + +- **Recommended structure (3 roles):** + 1) **Main function** orchestrates and wires output elements. + 2) **create* functions** declare output markup (tables/plots/text). + 3) **fill* (or compute*) functions** compute results and fill outputs. + +- **Dependencies (cache & reuse):** + Add `$dependOn()` to every output (table/plot/text/container/state) so JASP knows when to reuse or drop it. + Outputs nested within containers inherit all dependencies from the container. + +- **Errors:** + - Catch run-time errors with `try(...)` and report via `$setError()`. + - Wrap user-visible text with `gettext()` / `gettextf()` for translation. + +--- + +## 2) Input Validation + +Only validate the `dataset`. `options` input is validated in the QML automatically. + +Common checks (prefix arguments with the check name): +```r +.hasErrors( + dataset, type = c("factorLevels", "observations", "variance", "infinity", "missingValues"), + factorLevels.target = options$variables, + factorLevels.amount = "< 1", + observations.target = options$variables, + observations.amount = "< 1" +) +``` +Other useful checks: +- `limits.min/max` (inclusive bounds), +- `varCovData.target/corFun` (positive-definiteness), +- `modelInteractions` (ensure lower-order terms exist). + +--- + +## 3) Output Components + +### Tables — `createJaspTable()` +**Key methods/properties:** +- `$dependOn()` +- `$addCitation("")` +- `$addColumnInfo(name, title, type = "string|number|integer|pvalue", format = "sf:4;dp:3", combine = FALSE, overtitle = NULL)` +- `$showSpecifiedColumnsOnly <- TRUE` (hide unspecified stats you happen to compute) +- `$setExpectedSize(nRows)` (for long computations) +- `$addFootnote(message, colNames = NULL, rowNames = NULL)` +- `$addRows(list(...))` or `$setData(df)` +- `$setError("")` + +**Skeleton:** +```r +.createMyTable <- function(jaspResults, dataset, options, ready) { + if (!is.null(jaspResults[["mainTable"]])) return() + tab <- createJaspTable(title = gettext("My Table")) + tab$dependOn(c("variables", "alpha", "showCI")) + tab$addColumnInfo("variable", gettext("Variable"), "string", combine = TRUE) + tab$addColumnInfo("estimate", gettext("Estimate"), "number") + if (options$showCI) { + over <- gettextf("%f%% CI", 100 * options[["alpha"]]) + tab$addColumnInfo("lcl", gettext("Lower"), "number", overtitle = over) + tab$addColumnInfo("ucl", gettext("Upper"), "number", overtitle = over) + } + tab$showSpecifiedColumnsOnly <- TRUE + jaspResults[["mainTable"]] <- tab + if (!ready) return() + .fillMyTable(tab, dataset, options) +} +``` + +### Plots — `createJaspPlot()` +**Key methods/properties:** +- `$dependOn()`, `$addCitation()` +- Set `plotObject <- ggplot2::ggplot(...)` +- `$setError("")` + +**Skeleton:** +```r +.createMyPlot <- function(jaspResults, dataset, options, ready) { + if (!is.null(jaspResults[["descPlot"]])) return() + plt <- createJaspPlot(title = gettext("My Plot"), width = 400, height = 300) + plt$dependOn(c("variables", "alpha")) + jaspResults[["descPlot"]] <- plt + if (!ready) return() + .fillMyPlot(plt, dataset, options) +} +``` + +### Text blocks — `createJaspHtml()` +Display formatted messages; can depend on options like other outputs. +```r +if (!is.null(jaspResults[["note"]])) return() +msg <- createJaspHtml(text = gettextf("The variable %s was omitted.", options[["variable"]])) +msg$dependOn(c("variable")) +jaspResults[["note"]] <- msg +``` + +### Containers — `createJaspContainer()` +Group related outputs; container dependencies propagate to children. Useful for “one-per-variable” sections. +- `$dependOn(...)`, `$setError("")`, `$getError()` +- Nest containers freely. + +```r +if (is.null(jaspResults[["descGroup"]])) { + grp <- createJaspContainer(title = gettext("Descriptive Plots")) + grp$dependOn(c("variables", "alpha")) + jaspResults[["descGroup"]] <- grp +} else { + grp <- jaspResults[["descGroup"]] +} +for (v in options[["variables"]]) { + if (!is.null(grp[[v]])) next + p <- createJaspPlot(title = v, width = 480, height = 320) + p$dependOn(optionContainsValue = list(variables = v)) + grp[[v]] <- p +} +``` + +### State (cache) — `createJaspState()` +Cache computed results across reruns (while dependencies hold). +- `$dependOn(...)` +- `$object <- results` (store) / `results <- state$object` (retrieve) + +```r +.stateCompute <- function(jaspResults, dataset, options) { + st <- createJaspState() + st$dependOn(c("variables", "alpha")) + jaspResults[["internalResults"]] <- st + res <- colMeans(dataset[options[["variables"]]], na.rm = TRUE) + st$object <- res +} +``` + +--- + +## 4) Style & Conventions + +- **Follow the project R style guide.** Keep functions short; prefer pure helpers; avoid global state; no I/O or printing in analyses. +- **Naming:** + - Helpers start with a dot, e.g., `.computeFoo()`, `.fillBarTable()`, `.plotBaz()`. + - Stable keys in `jaspResults[["..."]]` (don’t rename them later). +- **Internationalization:** All visible text via `gettext()`/`gettextf()`. +- **Performance:** Read only needed columns; postpone decoding; reuse `createJaspState()` when multiple outputs share results. +- **Robustness:** Validate early; guard long loops with `if (!ready) return()`; wrap risky code in `try()` and call `$setError()`. +- **Reproducibility:** Set column formats explicitly in tables; document assumptions in footnotes/citations. +- **Assignment alignment:** +For related assignments allign them at the arrow `<-`, i.e., +``` +variableOne <- foo() +variableFive <- foo() +``` +and allign function arguments in the similar way for function whose call is too long to be on a single line: +``` +out <- foo( + argumentOne = variableOne, + argumentFive = variableFive, + ... +) + +--- + +## 5) Minimal main() template (copy/paste) + +```r +MyAnalysis <- function(jaspResults, dataset, options) { + + ready <- length(options[["variables"]]) > 0 + + .createMyTable(jaspResults, dataset, options, ready) + .createMyPlot(jaspResults, dataset, options, ready) +} diff --git a/.github/instructions/fix-debug-analysis.instructions.md b/.github/instructions/fix-debug-analysis.instructions.md new file mode 100644 index 0000000..f8cd931 --- /dev/null +++ b/.github/instructions/fix-debug-analysis.instructions.md @@ -0,0 +1,432 @@ +--- +applyTo: "**/R/*.R" +description: "Fixing bugs, debugging errors, and troubleshooting JASP analyses via code inspection and saveRDS state capture" +--- + +# Fix & Debug JASP Analysis (MCP Session) + +Quick reference for debugging JASP analysis functions through MCP sessions. + +**Note**: `browser()` and `recover()` require interactive R console and **do not work** through MCP's `btw_tool_run_r`. + +--- + +## 1) Debugging Approaches + +There are two approaches, in order of preference: + +### Approach A: Code Inspection (try first) + +Many bugs — especially logic errors, missing branches, wrong conditions — are solvable by reading the code and tracing the control flow. This is faster and doesn't require instrumenting code. + +1. **Reproduce**: Bootstrap a `runAnalysis()` call (Step 0) and confirm the issue +2. **Read**: Trace the code path from the entry-point function through the relevant helpers +3. **Identify**: Look for logic errors — wrong conditions, missing option checks, incorrect branching +4. **Fix**: Edit the source, hot-reload, and verify + +**Use this when**: Output is missing, wrong options are checked, a feature works in one analysis type but not another, UI options don't match R-side logic. + +### Approach B: saveRDS State Capture (escalation) + +When the bug depends on runtime values that can't be deduced from code reading alone. + +1. **Instrument**: Add saveRDS() before the error location +2. **Capture**: Hot-reload and run analysis, copy debug path from console +3. **Inspect**: Load saved state and examine values via MCP +4. **Fix**: Develop and test fix using captured state +5. **Verify**: Remove debug code, hot-reload, confirm fix works + +**Use this when**: Error depends on specific data values, unexpected NULL/type, dimension mismatches, or the code path is too complex to trace by reading. + +--- + +## 2) Reproducing the Issue + +### Step 0: Bootstrap a Reproducible Analysis Run + +Before debugging, you need a working `runAnalysis()` call that reproduces the error. Choose the first applicable source: + +#### Option A: User provides a .jasp file + +```r +jaspFile <- "path/to/file.jasp" +opts <- jaspTools::analysisOptions(jaspFile) +dataset <- jaspTools::extractDatasetFromJASPFile(jaspFile) +encoded <- jaspTools:::encodeOptionsAndDataset(opts, dataset) +set.seed(1) +results <- jaspTools::runAnalysis("AnalysisName", encoded$dataset, encoded$options, encodedDataset = TRUE, view = FALSE) +``` + +If the .jasp file contains multiple analyses, `analysisOptions()` returns a list — index with `[[1]]`, `[[2]]`, etc. Pick the analysis that matches the error context. + +#### Option B: Extract from existing unit tests (most common fallback) + +When no .jasp file is provided, **search test files first**. Test files contain pre-configured options and dataset references that are known to produce complete output. + +1. **Find the test file** for the analysis in `tests/testthat/`: + ``` + grep -r "AnalysisName" tests/testthat/ + ``` + +2. **Determine the input pattern** used in the test. Tests use one of two patterns: + + **Pattern 1 — .jasp example file** (look for `analysisOptions(jaspFile)` or `extractDatasetFromJASPFile`): + ```r + # Copy the loading code from the test, adjusting the path for non-test context + jaspFile <- file.path("examples", "Example Name.jasp") + opts <- jaspTools::analysisOptions(jaspFile)[[1]] # note: may need [[1]] for multi-analysis files + dataset <- jaspTools::extractDatasetFromJASPFile(jaspFile) + encoded <- jaspTools:::encodeOptionsAndDataset(opts, dataset) + set.seed(1) + results <- jaspTools::runAnalysis("AnalysisName", encoded$dataset, encoded$options, encodedDataset = TRUE, view = FALSE) + ``` + + **Pattern 2 — inline options** (look for `analysisOptions("AnalysisName")` with explicit option assignments): + ```r + # Copy the options setup from the test verbatim + options <- jaspTools::analysisOptions("AnalysisName") + options$dependent <- "contNormal" # copy from test + options$group <- "contBinom" # copy from test + # ... copy ALL option assignments from the test ... + set.seed(1) + results <- jaspTools::runAnalysis("AnalysisName", "debug.csv", options, view = FALSE) + ``` + +3. **Modify options** to match the bug-triggering scenario (e.g., enable/disable specific checkboxes). + +4. **Verify reproduction**: Check that the issue is reproduced — this could be a `"fatalError"` status, an error message in a specific output element, incorrect values, missing output, etc., depending on what the user reported. + +#### Option C: Build options from scratch (last resort) + +Only when no tests or examples exist: + +```r +options <- jaspTools::analysisOptions("AnalysisName") +# Set required inputs — check .robttCheckReady() or equivalent readiness function +# to discover which options must be non-empty +options$dependent <- "contNormal" +options$group <- "contBinom" +set.seed(1) +results <- jaspTools::runAnalysis("AnalysisName", "debug.csv", options, view = FALSE) +``` + +**Tip**: `jaspTools::analysisOptions("AnalysisName")` returns all options with their QML defaults. Inspect it with `str(options)` to understand available options and their types. + +--- + +## 3) saveRDS Workflow (Approach B) + +Use these steps when code inspection alone is insufficient and you need to examine runtime values. + +### Step 1: Identify Error Location + +From the error message and stack trace, locate the function and approximate line where the error occurs. + +**Example**: Stack trace shows `.buildTable()` → `table$addFootnote()` → error + +### Step 2: Instrument Code + +Add saveRDS() just **before** the line that's failing: + +```r +.buildTable <- function(jaspResults, options) { + # ... existing code ... + + someVariable <- computeSomething(data, options) + + # DEBUG: REMOVE - save state before error + debug_dir <- tempdir() + saveRDS(list( + someVariable = someVariable, + relatedData = relatedData, + fit = fit, + options = options + # Include ALL relevant variables + ), file.path(debug_dir, "debug_state.rds")) + message("DEBUG: Saved to ", file.path(debug_dir, "debug_state.rds")) + + # The line that's failing + processData(someVariable) +} +``` + +**Critical rules**: +- Always use marker comment `# DEBUG: REMOVE` +- **Never save `jaspResults`** (crashes R) +- Save to `tempdir()` (auto-cleanup) +- Include `message()` to print path to console +- Save ALL variables that might be relevant + +### Step 3: Hot-Reload and Capture + +```r +# Via btw_tool_run_r in MCP +devtools::load_all() + +# Re-run the analysis (use same code that triggered original error) +set.seed(1) +results <- jaspTools::runAnalysis("AnalysisName", encoded$dataset, encoded$options, encodedDataset = TRUE, view = FALSE) + +# Console output will show: +# DEBUG: Saved to C:/Users/.../Temp/RtmpXXX/debug_state.rds +``` + +Copy the debug path from the console output. + +### Step 4: Inspect Captured State + +```r +# Via btw_tool_run_r in MCP +debug_path <- "C:/Users/.../Temp/RtmpXXX/debug_state.rds" +debug_data <- readRDS(debug_path) + +# Examine structure +str(debug_data) + +# Inspect specific variables +print(debug_data$someVariable) +sapply(debug_data$someVariable, class) +any(sapply(debug_data$someVariable, is.null)) + +# Check attributes +for (i in seq_along(debug_data$relatedData)) { + cat("Item", i, "attribute:", attr(debug_data$relatedData[[i]], "someAttr"), "\n") +} +``` + +**Goal**: Identify the exact values causing the error. + +### Step 5: Develop Fix + +Based on inspection, develop fix logic using the saved objects: + +```r +# Via btw_tool_run_r in MCP +# Test the fix logic interactively using saved state + +# Example: Filter out invalid values +someVariable_clean <- Filter(function(x) !is.null(x) && is.finite(x), debug_data$someVariable) +print(someVariable_clean) # Verify it works + +# Try the fix +for (i in seq_along(someVariable_clean)) { + cat("Would process item:", someVariable_clean[[i]], "\n") +} +``` + +Once fix logic works, implement it in the source file. + +### Step 6: Clean Up and Verify + +1. Apply fix to source file +2. **Remove all debug code** (saveRDS(), message(), and "# DEBUG: REMOVE" markers) +3. Hot-reload and verify: + +```r +# Via btw_tool_run_r in MCP +devtools::load_all() + +set.seed(1) +results <- jaspTools::runAnalysis("AnalysisName", encoded$dataset, encoded$options, encodedDataset = TRUE, view = FALSE) + +# Check overall status +cat("Status:", results$status, "\n") +``` + +4. **Verify the specific issue is resolved** — don't just check `results$status`: + - If the bug was missing output: confirm the output element now exists in `results$results` + - If the bug was wrong values: check the specific table/cell values + - If the bug was an error in a subcomponent: navigate to that component and verify no error + - If the bug was a crash: confirm status is `"complete"` + +5. Search for any remaining debug code before committing: + +```bash +grep -r "DEBUG: REMOVE" R/ +grep -r "saveRDS.*tempdir" R/ +``` + +--- + +## 4) What to Save + +| Location | Objects to save | DON'T save | +|----------|----------------|------------| +| **Model fitting** | `dataset`, `options`, function args, intermediate values | `jaspResults`, `...` (ellipsis args) | +| **Row building** | `fit`, `attr(fit, "group")`, computed rows, `options` | Parent containers, environments | +| **Table assembly** | `rows` list, intermediate data.frames | Full fit objects if not needed | +| **Error handling** | Error object, variables being processed when error occurred | Large intermediate objects | + +**Golden rule**: When unsure, save it. Missing a variable means re-running the entire capture process. + +--- + +## 5) Real-World Example + +**Error**: `jaspTable$addFootnote expects 'message' to be a string!` + +**Workflow**: + +1. **Loaded .jasp file and reproduced error**: + ```r + jaspFile <- "path/to/file.jasp" + opts <- jaspTools::analysisOptions(jaspFile) + dataset <- jaspTools::extractDatasetFromJASPFile(jaspFile) + encoded <- jaspTools:::encodeOptionsAndDataset(opts, dataset) + set.seed(1) + results <- jaspTools::runAnalysis("AnalysisName", encoded$dataset, encoded$options, encodedDataset = TRUE, view = FALSE) + # → Status: fatalError + ``` + +2. **Identified error location**: Stack trace → `.buildTable()` at specific line + +3. **Instrumented code**: + ```r + footnotes <- unique(lapply(dataList, attr, which = "footnote")) + + # DEBUG: REMOVE + saveRDS(list( + footnotes = footnotes, + dataList = dataList + ), file.path(tempdir(), "footnote_debug.rds")) + message("DEBUG: Saved to ", file.path(tempdir(), "footnote_debug.rds")) + + for (i in seq_along(footnotes)) + table$addFootnote(footnotes[[i]]) + ``` + +4. **Captured state**: + ```r + devtools::load_all() + results <- jaspTools::runAnalysis(..., view = FALSE) + # Console: DEBUG: Saved to C:/Users/.../RtmpXXX/footnote_debug.rds + ``` + +5. **Inspected**: + ```r + debug_data <- readRDS("C:/Users/.../RtmpXXX/footnote_debug.rds") + str(debug_data$footnotes) + # List of 2 + # $ : chr "Some footnote text..." + # $ : NULL ← THE PROBLEM + ``` + +6. **Root cause**: `unique()` preserves NULL values → loop called `addFootnote(NULL)` → error + +7. **Implemented fix**: + ```r + footnotes <- unique(lapply(dataList, attr, which = "footnote")) + footnotes <- Filter(Negate(is.null), footnotes) # Filter NULLs + for (i in seq_along(footnotes)) + table$addFootnote(footnotes[[i]]) + ``` + +8. **Verified**: + ```r + devtools::load_all() + results <- jaspTools::runAnalysis(..., view = FALSE) + # → Status: complete ✓ + ``` + +**Time**: ~5 minutes from error to verified fix. + +--- + +## 6) Advanced Techniques + +### Conditional Saving + +For errors in specific iterations/groups: + +```r +# Only save when condition is met +for (i in seq_along(items)) { + if (i == 47) { # Error only in iteration 47 + saveRDS(list(item = items[[i]], i = i), file.path(tempdir(), "debug_iter47.rds")) + message("DEBUG: Saved iteration 47") + } + result <- process(items[[i]]) +} +``` + +### Multiple Checkpoints + +Narrow down error location by saving at multiple points: + +```r +# Checkpoint 1 +saveRDS(list(step = "before_transform", data = data), + file.path(tempdir(), "checkpoint1.rds")) + +data_transformed <- transform(data) + +# Checkpoint 2 +saveRDS(list(step = "after_transform", data_transformed = data_transformed), + file.path(tempdir(), "checkpoint2.rds")) +``` + +### Save with Timestamp + +For multiple runs: + +```r +timestamp <- format(Sys.time(), "%Y%m%d_%H%M%S") +saveRDS(list(...), file.path(tempdir(), paste0("debug_", timestamp, ".rds"))) +``` + +--- + +## 7) Common Error Patterns + +### Pattern 1: Unexpected NULL + +**Symptom**: "argument is NULL" or "expects X to be a Y" + +**Debugging**: +```r +saveRDS(list(suspect_var = suspect_var, related_vars = list(...)), ...) +# Inspect: is.null(debug_data$suspect_var) +``` + +### Pattern 2: Wrong Type/Class + +**Symptom**: "cannot coerce X to Y" or "is not a valid type" + +**Debugging**: +```r +saveRDS(list(var = var, class = class(var), str = capture.output(str(var))), ...) +# Inspect: class(debug_data$var), attributes(debug_data$var) +``` + +### Pattern 3: Dimension Mismatch + +**Symptom**: "dims [product X] do not match length of object [Y]" + +**Debugging**: +```r +saveRDS(list(obj = obj, dims = dim(obj), length = length(obj)), ...) +# Inspect: dim(debug_data$obj), length(debug_data$obj) +``` + +### Pattern 4: Index Out of Bounds + +**Symptom**: "subscript out of bounds" or "undefined columns selected" + +**Debugging**: +```r +saveRDS(list(container = container, index = i, length = length(container)), ...) +# Inspect: i vs length(debug_data$container), names(debug_data$container) +``` + +--- + +## 8) Safety Checklist + +Before committing code: + +- [ ] All `# DEBUG: REMOVE` markers removed +- [ ] All `saveRDS()` calls removed +- [ ] All debug `message()` calls removed +- [ ] Verified with: `grep -r "DEBUG: REMOVE" R/` +- [ ] Verified with: `grep -r "saveRDS.*tempdir" R/` +- [ ] Hot-reloaded and tested: analysis completes successfully diff --git a/.github/instructions/git-workflow.instructions.md b/.github/instructions/git-workflow.instructions.md new file mode 100644 index 0000000..36e83fc --- /dev/null +++ b/.github/instructions/git-workflow.instructions.md @@ -0,0 +1,205 @@ +--- +applyTo: "**" +description: "Git workflow: commit style, branch strategy, PR guidelines, safety rules" +--- + +# Git Workflow Instructions + +## Commit Message Style + +**Be extremely concise. Sacrifice grammar for concision.** + +### Format: +``` +: + +[optional body if needed] + +Co-Authored-By: Claude Sonnet 4.5 +``` + +### Types: +- `feat:` - New feature or analysis +- `fix:` - Bug fix +- `refactor:` - Code restructuring without behavior change +- `test:` - Adding or updating tests +- `docs:` - Documentation only +- `i18n:` - Translation updates +- `chore:` - Maintenance tasks + +### Examples: +``` +feat: add equivalence bounds plot + +fix: correct CI calculation in paired t-test + +test: update snapshots for descriptives table + +refactor: extract common validation logic + +i18n: update translation files +``` + +## Commit Workflow + +### 0. Ensure on feature branch: +```bash +# Check current branch +git branch + +# If on master, create feature branch +git checkout -b feature/descriptive-name +``` + +### 1. Before committing: +```bash +# Run full test suite +Rscript -e "library(jaspTools); agentTestAll()" + +# Check git status +git status + +# Review changes +git diff +``` + +### 2. Stage specific files: +```bash +# Stage specific files (preferred) +git add R/equivalenceonesamplettest.R +git add tests/testthat/test-equivalenceonesamplettest.R + +# Avoid staging everything unless you're certain +# git add -A # Be careful with this +``` + +### 3. Commit locally with co-author: +```bash +git commit -m "$(cat <<'EOF' +feat: add descriptives table + +Co-Authored-By: Claude Sonnet 4.5 +EOF +)" +``` + +**Local commits are OK. Pushing to remote requires human approval.** + +## Pre-Commit Requirements + +Before every commit, ensure: +- ✅ All tests pass (`jaspTools::agentTestAll()`) +- ✅ No unintended files staged (.env, credentials, etc.) +- ✅ Commit message is concise and descriptive +- ✅ Changes are focused and related + +## Branch Strategy + +- **Main branch:** `master` +- **NEVER work directly on `master` branch** +- **ALWAYS create a feature branch for any changes:** + ```bash + git checkout -b feature/descriptive-name + ``` +- Branch naming conventions: + - `feature/description` - New features or analyses + - `fix/description` - Bug fixes + - `refactor/description` - Code restructuring + - `test/description` - Test updates + +## Pull Request Guidelines + +**CRITICAL: NEVER push to remote, create PRs, or merge without explicit human approval.** + +Human must review all local changes before they go online. + +When human approves creating a PR: +1. Ensure all tests pass locally first +2. Keep PR scope focused and small +3. Use concise PR title (same style as commits) +4. Summarize changes in bullet points +5. Note any breaking changes +6. Wait for human to review the PR description before posting + +## What NOT to Commit + +- ❌ `.Rhistory`, `.RData`, `.Rproj.user/` +- ❌ Test artifacts or temporary files +- ❌ Personal IDE settings +- ❌ Large data files +- ❌ Credentials or API keys +- ❌ `CLAUDE.local.md` (personal preferences) + +## CI/CD Integration + +- GitHub Actions runs tests on every push +- Workflow file: `.github/workflows/unittests.yml` +- Tests must pass for PR to be merged +- Translation workflows run on schedule + +## Git Safety + +- **NEVER** work directly on `master` branch - always use feature branches +- **NEVER** push to remote without explicit human approval +- **NEVER** create pull requests without explicit human approval +- **NEVER** merge changes without explicit human approval +- **NEVER** force push to any branch +- **NEVER** amend published commits +- **NEVER** skip hooks unless explicitly needed +- **NEVER** commit without running tests first + +**Human must approve all changes before they go online.** + +## Common Git Commands + +```bash +# Check current branch +git branch + +# Create and switch to feature branch +git checkout -b feature/description + +# Check status +git status + +# View changes +git diff +git diff --staged + +# Stage specific files +git add + +# Commit locally (OK to do without approval) +git commit -m "message" + +# View recent commits +git log --oneline -5 + +# View commit history with graph +git log --graph --oneline --all -10 + +# === REQUIRE HUMAN APPROVAL BEFORE RUNNING: === + +# Push to remote (WAIT FOR APPROVAL) +git push origin feature/description + +# Pull latest changes (usually safe, but confirm first) +git pull origin master +``` + +## Handling Test Failures + +If CI tests fail after human has pushed: +1. Check GitHub Actions output +2. Reproduce failure locally +3. Fix the issue +4. Run tests to confirm fix +5. Commit locally +6. Ask human for approval to push fix + +## Translation Commits + +Translation updates are handled automatically: +- Weblate integration updates translation files +- Automated commits from translation workflow +- Don't manually edit translation files unless necessary diff --git a/.github/instructions/inst.qml.instructions.md b/.github/instructions/inst.qml.instructions.md new file mode 100644 index 0000000..83b4c08 --- /dev/null +++ b/.github/instructions/inst.qml.instructions.md @@ -0,0 +1,177 @@ +--- +applyTo: "**/inst/qml/*.qml" +description: "QML controls, validation, bindings, UI patterns, and qmllint syntax checking" +--- + +# JASP QML Instructions + +## 0) QML Syntax Validation + +**ALWAYS validate QML files after editing** using `qmllint` to catch syntax errors: + +```powershell +qmllint inst\qml\path\to\file.qml +``` + +- **Ignore import warnings**: Warnings about missing `JASP.Controls` and `JASP` modules are expected (qmllint lacks JASP's custom modules) +- **Focus on syntax errors**: Look for missing braces `{}`, brackets `[]`, parentheses `()`, semicolons, or malformed property assignments +- **Exit code matters**: Non-zero exit with syntax errors blocks parsing; zero exit means parseable (even with import warnings) +- **Run before committing**: Catch structural issues (extra/missing braces) that break QML parsing + +Example of ignorable warnings: +``` +Warning: Failed to import JASP.Controls [import] +Warning: IntegerField was not found [import] +``` + +Example of critical errors: +``` +Error: Expected token `}' [syntax] +``` + +## 1) Core Basics + +- **Imports:** + ```qml + import QtQuick + import QtQuick.Layouts + import JASP.Controls + import JASP + ``` + +- **Form as root:** Every analysis UI is a `Form { ... }` containing controls, usually a `VariablesForm` block and option controls. +- **Binding & IDs:** Prefer *property bindings* (reactive JS expressions) over imperative changes; reference other items via `id:` and bind (`enabled: show.checked || useAlt.checked`). +- **Stable storage names:** The `name:` of a control maps to stored options in JASP files; **avoid renaming**. If you must, handle migrations in `Upgrades.qml`. +- **Translation & docs:** + - Wrap **all user-visible strings** in `qsTr("Text")`. + - Populate `info:` with a short, user-facing description (also wrapped in `qsTr`) to feed module help. +- **Variables workflow:** Place variable pickers inside a `VariablesForm`; connect lists with `source:` (can read all data columns, other lists, levels, or R sources). + +## 2) Input Validation + +Prefer **declarative validation** via built-in field properties: + +- **Numeric fields** (`DoubleField`, `IntegerField`): set `min`, `max`, and `inclusive` (e.g., `MinMax`), `decimals` (for doubles), and allow negatives only when needed. Use `fieldWidth` for compact UI. +- **Percent & CI** (`PercentField`, `CIField`): sensible defaults (e.g., 95), `afterLabel` defaults to `"%"`. +- **Slider:** set `min`, `max`, `decimals`; prefer horizontal sliders unless space constrained. +- **FormulaField:** accepts R-style expressions; constrain with `min`, `max`, `inclusive`; use `multiple: true` only when arrays are intended. Read via `realValue` / `realValues`. +- **TableView:** for mixed types, define validators and override `getValidator(col,row)`; optionally specify `itemTypePerRow/Column`. +- **Variables lists:** enforce data types via `allowedColumns: ["scale"|"ordinal"|"nominal"]` and `singleVariable: true` where appropriate. + +## 3) Main Custom Components + +### General input +- **CheckBox** — `name`, `label`, `checked`, `childrenOnSameRow`, `columns` (nested controls auto-enable/disable). +- **RadioButtonGroup / RadioButton** — group has `name`, `title`, `radioButtonsOnSameRow`, `columns`; each button has `value`, `label`, `checked`; can contain nested controls per choice. +- **DropDown** — `name`, `label`, `values` (array or `{label, value}`), or `source`; selection via `startValue` / `currentValue`; `addEmptyValue`, `placeHolderText`. +- **Slider** — `name`, `label`, `value`, `min`, `max`, `decimals`. +- **DoubleField / IntegerField** — `label`, `defaultValue`, `min`, `max`, `inclusive`, (`decimals` for DoubleField). +- **PercentField / CIField** — percent-specific shorthand; defaults appropriate for CIs. +- **TextField** — `defaultValue` or `placeholderText` (mutually exclusive), `afterLabel`, `fieldWidth`. +- **FormulaField** — adds `realValue`, `min/max`, `inclusive`, `multiple`, `realValues`. +- **TextArea** — `title`, `text`, `textType` (e.g., R code / JAGS / Lavaan / Model / Source), `separator(s)`, `applyScriptInfo` (submit with **Ctrl+Enter**). + +### Variable specification +- **AvailableVariablesList** — `name`, `label`, **rich `source`** (other lists, levels, filters, `rSource`, combinations), or `values`; `width`, `count` (read-only). +- **AssignedVariablesList** — `name`, `label`, `allowedColumns`, `singleVariable`, `maxRows`, `listViewType` (e.g., `Interaction`), optional `rowComponent` (+ `rowComponentTitle`), `optionKey`, `count`. +- **FactorLevelList** — define RM factors/levels: `factorName`, `levelName`, `minFactors`, `minLevels`, `width`, `height`. Often paired with an `AssignedVariablesList` of type `MeasuresCells`. + +### Complex composition +- **ComponentsList** — templated rows of controls from a `source` or `values`; `titles`, `rowComponent`, manual rows via `addItemManually`, bounds via `minimumItems` / `maximumItems`, collected under `optionKey`. +- **TabView** — `ComponentsList` rendered as tabs. +- **InputListView** — user adds rows via an input field; `title`, `placeHolder`, `defaultValues`, `minRows`, `inputComponent` (Text/Double/Integer), optional `rowComponent`, `optionKey`. +- **TableView** — `name`, `modelType` (`MultinomialChi2Model`, `JAGSDataInputModel`, `FilteredDataEntryModel`, `CustomContrasts`), `itemType` or per-row/column types, `source`; may override `getColHeaderText`, `getRowHeaderText`, `getDefaultValue`, `getValidator`. + +### Grouping & structure +- **Group** — logical block with `title`, `columns`. Nest options inside. +- **Section** — collapsible panel for advanced options; `title`, `columns`. Use for lower-priority / expert settings. + +## 4) Style & UX Conventions + +- **Titles & labels:** Title Case for section/group titles; concise labels; every visible string uses `qsTr()`. The `name` is always the title transformed into camelCase. Options within groups inherit their names as a prefix. +- **Consistency:** Prefer the provided JASP controls over ad-hoc QML; nest subordinate options inside the control that enables them (e.g., a `CheckBox` containing its dependent fields). +- **Two-column rhythm:** Let the grid flow naturally; use `rowSpan/columnSpan` to avoid awkward gaps; avoid long single-column scrollers. +- **Variables first:** Place `VariablesForm` at the top; align list widths; restrict types with `allowedColumns`. +- **Defaults & placeholders:** Prefer meaningful `defaultValue`; use `placeholderText` only when input is optional. Don’t set both. +- **Dropdowns:** Use `{label, value}` pairs when R-side value differs; add an explicit empty choice with `addEmptyValue` if “no selection” is valid. +- **Advanced options:** Tuck rare/expert settings into a `Section` titled “Advanced Options”. +- **Docs:** Fill `info:` succinctly for every major control. +- **Spacing:** Always use tabs for spacing. Each argument on a new line. (See examples below.) + +## 5) Quick Patterns + +- **Enable dependent field(s):** + ```qml + CheckBox + { + id: show + name: "showX" + label: qsTr("Show X") + } + + DoubleField + { + name: "Alpha" + label: qsTr("Alpha") + defaultValue: 0.05 + min: 0 + max: 1 + decimals: 3 + enabled: show.checked + } + ``` + +- **Radio choice with per-choice inputs:** + ```qml + RadioButtonGroup + { + name: "crit" + title: qsTr("Criterion") + + RadioButton + { + value: "pValue" + label: qsTr("p-value") + checked: true + + DoubleField + { + name: "pValueValue" + label: "" + defaultValue: 0.05 + min: 0 + max: 1 + } + } + + RadioButton + { + ... + } + } + ``` + +- **Variables form (single DV):** + ```qml + VariablesForm + { + AvailableVariablesList + { + name: "availableVariables" + } + + AssignedVariablesList + { + name: "dependentVariable" + label: qsTr("Dependent Variable") + allowedColumns: ["scale"] + singleVariable: true + } + } + ``` + +## 6) When in doubt + +- Prefer built-in JASP controls. +- Keep `name:` stable; translate strings; validate inputs. +- Put rare/expert options in a `Section` and document via `info:`. diff --git a/.github/instructions/jasp-containers-and-errors.instructions.md b/.github/instructions/jasp-containers-and-errors.instructions.md new file mode 100644 index 0000000..9ab911e --- /dev/null +++ b/.github/instructions/jasp-containers-and-errors.instructions.md @@ -0,0 +1,146 @@ +--- +applyTo: "**/R/*.R" +description: "Container patterns, HTML output, and error handling in jaspResults" +--- + +# JASP Containers, HTML Output & Error Handling + +Patterns for grouping output elements and handling errors in jaspResults. + +For tables see [jasp-tables.md](jasp-tables.md). +For plots see [jasp-plots.md](jasp-plots.md). +For state/caching see [jasp-state-management.md](jasp-state-management.md). + +--- + +## 1) Containers + +Containers group related output elements under a collapsible section. + +### Get-or-create pattern (reusable across multiple builder functions) + +```r +.myExtractContainer <- function(jaspResults) { + if (!is.null(jaspResults[["myContainer"]])) + return(jaspResults[["myContainer"]]) + + container <- createJaspContainer(gettext("My Section Title")) + container$dependOn(.myBaseDependencies) + container$position <- 1 + jaspResults[["myContainer"]] <- container + + return(container) +} +``` + +- Use a dedicated extractor when **multiple builder functions** write to the same container +- `$position` controls display order (lower = higher on page) +- `$dependOn()` on the container invalidates **all children** when base options change + +### Direct creation (when only one function writes to it) + +```r +if (is.null(jaspResults[["sectionContainer"]])) { + container <- createJaspContainer(gettext("Section Title")) + container$dependOn(c(.baseDependencies, "specificOption")) + container$position <- 4 + jaspResults[["sectionContainer"]] <- container +} +``` + +### Nested containers + +For deeply hierarchical output (e.g., per-variable tables): + +```r +outerContainer <- jaspResults[["outer"]] +innerContainer <- createJaspContainer(title = "Variable X") +innerContainer$position <- i +outerContainer[["variableX"]] <- innerContainer +# then add tables/plots to innerContainer +``` + +### Dynamic container management + +When the set of children depends on user-selected variables: + +```r +# Track existing vs selected variables via metadata state +existingVariables <- metaData[["existingVariables"]] +selectedVariables <- getSelectedVariables(options) + +# Remove deselected +for (v in setdiff(existingVariables, selectedVariables)) + container[[v]] <- NULL + +# Add new +for (v in setdiff(selectedVariables, existingVariables)) { + childContainer <- createJaspContainer(title = v) + container[[v]] <- childContainer + .buildChildTable(childContainer, fit, options, v) +} + +# Update metadata +metaDataState$object <- list(existingVariables = selectedVariables) +``` + +See [jasp-state-management.md](jasp-state-management.md) for the metadata state pattern that powers this. + +--- + +## 2) HTML Output + +For raw HTML content (e.g., displaying R code or formatted messages): + +```r +htmlOutput <- createJaspHtml(title = gettext("R Code")) +htmlOutput$dependOn(c(.baseDependencies, "showCode")) +htmlOutput$position <- 99 +htmlOutput$text <- "
myFunction(yi = ..., sei = ...)
" +jaspResults[["rCode"]] <- htmlOutput +``` + +--- + +## 3) Error Handling Patterns + +### Create-then-error + +Always **attach the element to jaspResults before checking errors**. This ensures the empty table (with error message) is displayed rather than nothing: + +```r +table <- createJaspTable(gettext("Title")) +container[["table"]] <- table # attach FIRST + +# THEN check for errors +if (someError) { + table$setError(errorMessage) + return() +} +``` + +### Graceful degradation with groups + +When some per-group fits fail but others succeed, show partial results with per-group error footnotes: + +```r +# Row builders return skeleton data.frames on error (labels only, NAs for numeric columns) +# Tables show partial results with error footnotes per failed group +for (i in which(sapply(fit, jaspBase::isTryError))) { + table$addFootnote( + gettextf("Group '%1$s' failed: %2$s", attr(fit[[i]], "group"), .cleanError(fit[[i]])), + symbol = gettext("Error:") + ) +} +``` + +### Total failure + +When the entire fit fails: + +```r +if (length(fit) == 1 && jaspBase::isTryError(fit[[1]])) { + table$setError(.cleanErrorMessage(fit[[1]])) + return() +} +``` diff --git a/.github/instructions/jasp-dependency-management.instructions.md b/.github/instructions/jasp-dependency-management.instructions.md new file mode 100644 index 0000000..93eb8db --- /dev/null +++ b/.github/instructions/jasp-dependency-management.instructions.md @@ -0,0 +1,136 @@ +--- +applyTo: "**/R/*.R" +description: "dependOn mechanics, inheritance, vectors, per-value deps, sentinel pattern" +--- + +# JASP Dependency Management ($dependOn) + +How `$dependOn()` controls caching and invalidation of output elements in jaspResults. + +For the reactive loop context see [jasp-module-architecture.md](jasp-module-architecture.md). + +Note that you cannot test this by running analysis via `runAnalysis(..., view = FALSE)` because you only generate one state at a time +(with no initial elements - ask the human maintainer to validate the dependencies manually if you suspect an issue!). + +--- + +## 1) What $dependOn Does + +When you write: +```r +table$dependOn(c("method", "ciLevel")) +``` + +You tell JASP Desktop: "If `options[["method"]]` or `options[["ciLevel"]]` changes, set this element to NULL before calling R." On the next R invocation, the builder's `if (!is.null(...))` guard sees NULL and recreates the element. + +Elements whose dependencies are NOT hit survive across invocations -- the builder returns early and the existing output stays on screen. + +--- + +## 2) Dependency Inheritance + +Container dependencies propagate to ALL children: + +```r +container$dependOn(c("dependentVariable", "method")) # base deps +table$dependOn(c("showCI")) # additional dep +container[["myTable"]] <- table +``` + +The table is invalidated if `dependentVariable`, `method`, OR `showCI` changes. Never repeat parent deps on children. + +This means you can put shared model-level dependencies on the container and only add output-specific deps to individual tables/plots. + +--- + +## 3) Dependency Vectors as Constants + +Define at file top for reuse across builders: +```r +.baseDeps <- c("dependentVariable", "covariates", "method", "ciLevel") +.plotDeps <- c("plotColor", "plotSize", "plotTheme") +``` + +Use in builders: +```r +container$dependOn(.baseDeps) # container holds base deps +table$dependOn(c("showResiduals")) # child adds specific dep +plot$dependOn(c(.baseDeps, .plotDeps)) # or combine for standalone elements +``` + +Keep dependency vectors comprehensive -- missing a dependency means stale output when that option changes. + +--- + +## 4) Conditional / Dynamic Dependencies + +When different analysis modes need different dependency sets: +```r +if (options[["variant"]] == "classical") { + fitState$dependOn(.classicalDeps) +} else { + fitState$dependOn(.bayesianDeps) +} +``` + +Or combine dynamically: +```r +plot$dependOn(c(.plotDeps, + if (options[["variant"]] == "classical") .classicalDeps else .bayesianDeps +)) +``` + +--- + +## 5) Per-Value Dependencies (optionContainsValue) + +For containers with one child per user-selected variable, invalidate only when that specific variable is removed: + +```r +for (v in options[["variables"]]) { + if (!is.null(container[[v]])) next + plot <- createJaspPlot(title = v) + plot$dependOn(optionContainsValue = list(variables = v)) + container[[v]] <- plot + # ... fill plot ... +} +``` + +If the user removes variable `"x"` from the list, only `container[["x"]]` is NULLed. Other children survive. + +--- + +## 6) Sentinel Pattern (Narrow Dependencies) + +When an expensive computation (e.g., model fit) should NOT be invalidated by visualization-only options, but the visualization data still needs updating: + +```r +# Broad deps: model options → invalidate and re-fit +fitState <- createJaspState() +fitState$dependOn(.modelDeps) +jaspResults[["fit"]] <- fitState + +# Narrow deps: plotting options → update auxiliary data without re-fitting +sentinel <- createJaspState() +sentinel$dependOn(.plottingDeps) +jaspResults[["fitDataUpdate"]] <- sentinel +``` + +When a plotting option changes: +- `jaspResults[["fit"]]` survives (model deps not hit) +- `jaspResults[["fitDataUpdate"]]` is NULLed (plotting deps hit) +- The update function sees the NULL sentinel, re-attaches updated auxiliary data to the existing fit + +This avoids expensive re-computation when only display options change. + +--- + +## 7) Common Pitfalls + +**Missing dependency:** If you forget to list an option in `$dependOn()`, changing that option won't invalidate the element. The user sees stale output. + +**Over-broad dependencies:** Putting ALL options on every element means everything gets recomputed on any change. Split into base deps (container) + specific deps (children). + +**Duplicate dependencies:** Listing a parent container's dep on a child is harmless but redundant. Keep it clean. + +**Forgetting $dependOn entirely:** The element will never be invalidated -- it's created once and persists forever, even when relevant options change. diff --git a/.github/instructions/jasp-module-architecture.instructions.md b/.github/instructions/jasp-module-architecture.instructions.md new file mode 100644 index 0000000..15fe5f7 --- /dev/null +++ b/.github/instructions/jasp-module-architecture.instructions.md @@ -0,0 +1,272 @@ +--- +applyTo: "**/R/*.R,**/inst/qml/*.qml" +description: "QML-Desktop-R reactive loop, jaspResults persistence, options mapping, data flow" +--- + +# JASP Module Architecture + +How QML, JASP Desktop, and R interact. This explains *why* the patterns in the other rule files exist. + +For dependency details see [jasp-dependency-management.md](jasp-dependency-management.md). +For state/caching see [jasp-state-management.md](jasp-state-management.md). +For R coding patterns see [jasp-tables.md](jasp-tables.md), [jasp-plots.md](jasp-plots.md), [jasp-containers-and-errors.md](jasp-containers-and-errors.md). +For serialized output format see [jasp-output-structure.md](jasp-output-structure.md). + +--- + +## 1) The Reactive Loop + +``` +User changes option in QML GUI + │ + ▼ +JASP Desktop collects ALL current option values into a flat named list + │ + ▼ +Desktop calls: AnalysisName(jaspResults, dataset, options) + │ │ │ │ + │ │ │ └─ named list of ALL QML option values + │ │ └─ data.frame loaded from the active dataset + │ └─ PERSISTENT container surviving across invocations + │ + ▼ +R function builds/updates output in jaspResults + │ + ▼ +Desktop reads jaspResults and renders tables/plots/text in the GUI +``` + +**Key insight:** Every time the user changes *anything* in the QML interface, Desktop calls the R analysis function again with a fresh `options` list but the **same** `jaspResults` object. This is why: + +1. Every builder checks `if (!is.null(jaspResults[["key"]])) return()` -- skip if output already exists and dependencies haven't changed. +2. `$dependOn()` tells Desktop which option changes should invalidate (NULL out) an element. See [jasp-dependency-management.md](jasp-dependency-management.md). +3. `createJaspState()` caches expensive computations so they survive across invocations. See [jasp-state-management.md](jasp-state-management.md). + +--- + +## 2) jaspResults: The Persistent Bridge + +`jaspResults` is an R5 reference class that persists between R invocations for the same analysis instance. It is NOT recreated each time. + +### Element lifecycle + +``` +1. Element does not exist → builder creates it, attaches to jaspResults +2. Options change, deps NOT hit → element survives, builder returns early +3. Options change, deps ARE hit → Desktop NULLs the element before calling R + → builder sees NULL, recreates it +4. User removes the analysis → jaspResults is destroyed entirely +``` + +### What can live in jaspResults + +| Create function | Purpose | Displayed? | +|----------------|---------|------------| +| `createJaspTable()` | Tabular output | Yes | +| `createJaspPlot()` | Plot output | Yes | +| `createJaspHtml()` | Raw HTML/text | Yes | +| `createJaspContainer()` | Groups children | Yes (collapsible section) | +| `createJaspState()` | Cache arbitrary R objects | **No** (invisible to user) | + +All five support `$dependOn()`. All five can be stored in jaspResults or nested inside a container. + +### Display ordering + +Every element has `$position` (integer). Lower = higher on page. Children within a container also have positions. + +--- + +## 3) Options: The Flat Named List + +### QML name → R options key + +Every QML control has a `name:` property. Desktop flattens ALL controls into a single named list regardless of QML nesting: + +```qml +CheckBox { + name: "showCI" // options[["showCI"]] = TRUE/FALSE + DoubleField { + name: "ciLevel" // options[["ciLevel"]] = 0.95 + defaultValue: 0.95 + } +} +``` + +Both `showCI` and `ciLevel` appear at the top level of `options`. QML nesting controls UI visibility/enabling but does NOT create nested R structures. + +### QML control → R value type + +| QML control | R type | Example value | +|-------------|--------|---------------| +| `CheckBox` | logical | `TRUE` / `FALSE` | +| `DropDown` | character | `"restrictedML"` | +| `RadioButtonGroup` | character | `"estimated"` (selected button's `value:`) | +| `AssignedVariablesList` | character | `"myColumn"` (single) or `c("a","b")` (multi) | +| `DoubleField` | numeric | `0.95` | +| `IntegerField` | integer | `1000L` | +| `TextField` | character | `"user text"` | +| `CIField` | numeric | `0.95` (0-1 scale) | +| `PercentField` | numeric | `95` (0-100 scale) | + +### Empty/unset variable slots + +When no variable is assigned to an `AssignedVariablesList`, the value is `""` (empty string): + +```r +if (options[["dependentVariable"]] != "") { ... } +``` + +For multi-variable lists, check `length(options[["variables"]]) > 0`. + +### Column encoding + +JASP internally encodes column names. In R analysis code, the encoding is transparent -- `dataset` columns are already encoded. Use `jaspBase::decodeColNames()` when displaying names in plot axes/labels. In tests, use `jaspTools:::encodeOptionsAndDataset()` when loading from .jasp files. + +--- + +## 4) Data Flow (Generic) + +``` +QML assigns variable names → options[["dependentVariable"]] = "score" + │ + ▼ +Desktop loads dataset with requested columns → dataset (data.frame) + │ + ▼ +Entry point: readiness check + data validation + - Are required variables assigned? + - .hasErrors(): infinity, observations, variance, etc. + │ + ▼ +Compute function: expensive model fitting, cached in state + - Wrap in try() for error handling + - Store result via createJaspState() + │ + ▼ +Builder functions: extract cached results, build output + - Tables: define columns, build rows, setData() + - Plots: build ggplot, assign to plotObject + - Errors: attach element FIRST, then setError() +``` + +Builders should handle the "not ready" case gracefully -- create empty tables (column headers but no data) so the user sees the output structure before assigning variables. + +--- + +## 5) The Entry Point → Common → Builder Pattern + +### Three-layer architecture + +``` +Layer 1: Entry point (thin wrapper per analysis) + MyAnalysis(jaspResults, dataset, options) + - Sets dispatch flags if sharing code with other analyses + - Validates data + - Delegates to orchestrator + +Layer 2: Orchestrator (flat sequence of builder calls) + MyAnalysisCommon(jaspResults, dataset, options) + - Calls .computeModel() # state + - Calls .summaryTable() # table + - Calls .coefficientsTable() # table + - Calls .mainPlot() # plot + - Conditional sections based on options + +Layer 3: Builders (idempotent, self-contained) + .summaryTable(jaspResults, options) + - Checks if output exists (return early if so) + - Gets/creates container + - Creates table, defines columns + - Extracts cached results + - Builds rows, sets data +``` + +### Multiple entry points sharing one orchestrator + +When related analyses share logic, they set a dispatch flag and delegate: + +```r +AnalysisVariantA <- function(jaspResults, dataset, options) { + options[["variant"]] <- "A" + if (.isReady(options)) { + dataset <- .checkData(dataset, options) + .checkErrors(dataset, options) + } + AnalysisCommon(jaspResults, dataset, options) +} + +AnalysisVariantB <- function(jaspResults, dataset, options) { + options[["variant"]] <- "B" + # ... same pattern ... + AnalysisCommon(jaspResults, dataset, options) +} +``` + +Builders branch on the flag: +```r +if (options[["variant"]] == "B") + .additionalTable(jaspResults, options) +``` + +### The readiness check + +Before model fitting, verify required inputs exist: + +```r +.isReady <- function(options) { + options[["dependentVariable"]] != "" && length(options[["covariates"]]) > 0 +} +``` + +In the entry point: +```r +if (.isReady(options)) { + dataset <- .checkData(dataset, options) + .checkErrors(dataset, options) +} +AnalysisCommon(jaspResults, dataset, options) +``` + +--- + +## 6) Registration & Backward Compatibility + +### Description.qml + +Registers analyses with their R function names: +```qml +Analysis { + title: qsTr("My Analysis") + func: "MyAnalysis" // must match R function name exactly (case-sensitive) +} +``` + +### NAMESPACE + +Every analysis entry point must be exported: +```r +export(MyAnalysis) +``` + +### Upgrades.qml + +When renaming QML option names, add a migration so old .jasp files load correctly: +```qml +Upgrade { + functionName: "MyAnalysis" + fromVersion: "0.17.2" + toVersion: "0.17.3" + + ChangeRename { from: "oldOptionName"; to: "newOptionName" } + + ChangeJS { + name: "transformedOption" + jsFunction: function(options) { + switch(options["transformedOption"]) { + case "oldValue": return "newValue"; + default: return options["transformedOption"]; + } + } + } +} +``` diff --git a/.github/instructions/jasp-output-structure.instructions.md b/.github/instructions/jasp-output-structure.instructions.md new file mode 100644 index 0000000..439851c --- /dev/null +++ b/.github/instructions/jasp-output-structure.instructions.md @@ -0,0 +1,196 @@ +--- +applyTo: "**/tests/testthat/*.R,**/R/*.R" +description: "Reading and testing serialized output from runAnalysis (containers, tables, plots, state)" +--- + +# JASP Analysis Output Structure + +Reading and testing the serialized output from `jaspTools::runAnalysis()`. +For building tables see [jasp-tables.md](jasp-tables.md). For plots see [jasp-plots.md](jasp-plots.md). +When you run it manually, use `view = FALSE` so JASP skips HTML generation and you can inspect the returned R object directly. + +## 1) Top-Level `results` Object + +After `jaspTools::runAnalysis(..., view = FALSE)`, the returned list has 5 keys: +- `status` -- `"complete"` or `"fatalError"` +- `results` -- nested list of all output elements (containers, tables, plots) +- `state` -- cached figures and computed objects +- `progress` -- progress info (usually empty after completion) +- `typeRequest` -- internal type info + +## 2) `results$results` Structure + +Contains: +- `.meta` -- recursive metadata describing the tree (type, name, title for each element) +- `name` -- analysis name +- Named elements for each output component (containers, tables, plots) + +### Element Types + +| Type | Key fields | How to identify | +|------|-----------|-----------------| +| **Container** | `collection`, `name`, `title`, `initCollapsed` | Has `$collection` (named list of children) | +| **Table** | `data`, `schema`, `name`, `title`, `status`, `footnotes`, `casesAcrossColumns` | Has `$schema` with `$fields` | +| **Plot/Image** | `data` (string path), `name`, `title`, `width`, `height`, `status`, `convertible` | Has `$data` as character string (e.g., `"plots/1.png"`) | + +## 3) Containers + +Containers group related output elements. Structure: +``` +container$collection -- named list of child elements (containers, tables, or plots) +container$name -- unique identifier (underscore-separated path) +container$title -- display title (can be "") +container$initCollapsed -- whether collapsed by default +``` + +**Naming convention:** Child names are parent name + `_` + child suffix. This creates a hierarchical path: +``` +modelSummaryContainer + modelSummaryContainer_testsTable + modelSummaryContainer_pooledEstimatesTable +``` + +Containers can nest arbitrarily deep: +``` +estimatedMarginalMeansAndContrastsContainer + estimatedMarginalMeansAndContrastsContainer_effectSize + estimatedMarginalMeansAndContrastsContainer_effectSize_adjustedEstimate + ..._adjustedEstimate_estimatedMarginalMeansTable +``` + +**Accessing deeply nested elements:** Chain `$collection` at each container level: +```r +results[["results"]][["containerName"]][["collection"]][["containerName_child"]][["collection"]][["containerName_child_table"]][["data"]] +``` + +## 4) Tables + +### Schema (`table$schema$fields`) +List of column definitions, each with: +- `name` -- field identifier (used as key in data rows) +- `title` -- display column header +- `type` -- `"string"`, `"number"`, `"integer"`, `"pvalue"` +- `format` (optional) -- formatting spec, e.g., `"sf:4;dp:3"`, `"dp:3;p:.001"` +- `overTitle` (optional) -- grouped column header (e.g., `"95% CI"` spanning Lower/Upper) + +### Data (`table$data`) +List of rows. Each row is a named list with field names as keys: +```r +table$data[[1]] # first row +# $est, $se, $lCi, $uCi, $pval, ... +``` + +**Key:** Fields within each row are **alphabetically sorted by name** (from JSON deserialization). + +### Footnotes (`table$footnotes`) +List of footnote objects: +```r +footnote$text -- footnote text +footnote$symbol -- HTML symbol (e.g., "Note.") +footnote$cols -- columns it applies to (NULL = all) +footnote$rows -- rows it applies to (NULL = all) +``` + +### Special Row Fields +- `.isNewGroup` -- boolean, marks visual row separator in JASP GUI +- These appear in `expect_equal_tables` flattened output + +## 5) Plots + +### In `results$results` +Plot entries store metadata only: +```r +plot$data -- string key into state$figures (e.g., "plots/1.png") +plot$name -- identifier +plot$title -- display title +plot$width -- pixel width +plot$height -- pixel height +plot$status -- "complete" +``` + +### In `results$state$figures` +Actual plot objects stored here, keyed by the `data` path: +```r +results$state$figures[["plots/1.png"]]$obj -- the plot object +results$state$figures[["plots/1.png"]]$width +results$state$figures[["plots/1.png"]]$height +``` + +### Plot Object Types +- **`jaspGraphsPlot`** (R6 class) -- composite plot with `$subplots` list of ggplot objects +- **Plain `ggplot`** -- single ggplot object (no subplots) + +### Retrieving Plot for Testing +```r +plotName <- results[["results"]][["plotElement"]][["data"]] +testPlot <- results[["state"]][["figures"]][[plotName]][["obj"]] +jaspTools::expect_equal_plots(testPlot, "snapshot-name") +``` + +## 6) State Object (`results$state`) + +- `state$figures` -- named list of plot objects (keyed by "plots/N.png") +- `state$other` -- named list of cached R objects (keyed by "state_N") + - Used by `createJaspState()` for caching expensive computations between output elements + +## 7) Testing Utilities + +### `expect_equal_tables(table_data, reference_list)` +1. Takes `table$data` (list of row-lists) +2. Flattens via `unname(unlist(rows))` -- row-by-row, fields in alphabetical order within each row +3. Converts numeric strings back to numbers via `charVec2MixedList` +4. Replaces unicode characters with `` placeholder +5. Compares element-by-element against flat reference list + +**Reference list format:** Single flat `list(...)` with all values row-by-row, fields alphabetically sorted: +```r +# For a table with fields: df, est, name, pval (alphabetical) +# Row 1: df=9, est=-0.69, name="Intercept", pval=0.50 +# Row 2: df=9, est=0.29, name="Slope", pval=0.01 +jaspTools::expect_equal_tables(table_data, + list(9, -0.69, "Intercept", 0.50, # row 1 + 9, 0.29, "Slope", 0.01)) # row 2 +``` + +### `expect_equal_plots(plot_obj, snapshot_name)` +- If `jaspGraphsPlot`: splits into subplots, each compared via `vdiffr::expect_doppelganger` with name `"snapshot-name-subplot-N"` +- If plain `ggplot`: compared directly via `vdiffr::expect_doppelganger` +- SVG snapshots stored in `tests/testthat/_snaps/` + +## 8) Quick Reference: Navigating Results + +```r +# Run analysis +results <- jaspTools::runAnalysis("AnalysisName", dataset, options, view = FALSE) + +# Check status +results$status # "complete" or "fatalError" +results$results$errorMessage # if fatalError + +# Get table data (for expect_equal_tables) +results[["results"]][["containerName"]][["collection"]][["containerName_tableName"]][["data"]] + +# Get plot object (for expect_equal_plots) +plotKey <- results[["results"]][["plotName"]][["data"]] +plotObj <- results[["state"]][["figures"]][[plotKey]][["obj"]] + +# Inspect table schema +table$schema$fields # list of {name, title, type, format, overTitle} + +# Map entire tree (debug helper) +mapResults <- function(x, depth = 0) { + indent <- paste(rep(" ", depth), collapse = "") + if (is.list(x) && !is.null(x$collection)) { + cat(sprintf("%s[container] %s: '%s'\n", indent, x$name, x$title)) + for (child in x$collection) mapResults(child, depth + 1) + } else if (is.list(x) && !is.null(x$schema)) { + cat(sprintf("%s[table] %s: '%s' (%d rows x %d cols)\n", + indent, x$name, x$title, length(x$data), length(x$schema$fields))) + } else if (is.list(x) && !is.null(x$data) && is.character(x$data)) { + cat(sprintf("%s[plot] %s: '%s'\n", indent, x$name, x$title)) + } +} +for (item in results$results[setdiff(names(results$results), c(".meta", "name"))]) { + mapResults(item) +} +``` diff --git a/.github/instructions/jasp-plots.instructions.md b/.github/instructions/jasp-plots.instructions.md new file mode 100644 index 0000000..ad59db3 --- /dev/null +++ b/.github/instructions/jasp-plots.instructions.md @@ -0,0 +1,136 @@ +--- +applyTo: "**/R/*.R" +description: "Plot lifecycle, composite plots, subgroup/facet patterns in jaspResults" +--- + +# JASP Plot Building Patterns + +How to create and configure plots in jaspResults. + +For dependency mechanics see [jasp-dependency-management.md](jasp-dependency-management.md). +For testing plots see [testing.instructions.md](testing.instructions.md) (`expect_equal_plots`). + +--- + +## 1) Simple Plot + +```r +.myPlot <- function(jaspResults, options) { + + if (!is.null(jaspResults[["myPlot"]])) + return() + + fit <- .extractFit(jaspResults, options) + if (is.null(fit) || jaspBase::isTryError(fit[[1]])) + return() + + myPlot <- createJaspPlot( + title = gettext("My Plot"), + width = 400, + height = 320 + ) + myPlot$position <- 5 + myPlot$dependOn(c(.baseDependencies, "plotSpecificOption")) + jaspResults[["myPlot"]] <- myPlot + + # Build ggplot + plotObj <- ggplot2::ggplot(...) + ... + + # Add JASP theme and (plot frame b = bottom, r = right, t = top, l = left) + plotObj <- plotObj + + jaspGraphs::geom_rangeframe(sides = "bl") + + jaspGraphs::themeJaspRaw() + + myPlot$plotObject <- plotObj +} +``` + +--- + +## 2) Plot with Error Handling + +Wrap plot construction in `try()` and display the error on the plot element: + +```r +plotOut <- try(.makePlot(fit, options)) + +if (inherits(plotOut, "try-error")) { + myPlot <- createJaspPlot(title = gettext("My Plot")) + myPlot$dependOn(dependencies) + myPlot$setError(plotOut) + jaspResults[["myPlot"]] <- myPlot + return() +} + +myPlot <- createJaspPlot(title = gettext("My Plot"), width = w, height = h) +myPlot$plotObject <- plotOut +jaspResults[["myPlot"]] <- myPlot +``` + +--- + +## 3) Composite Plot (jaspGraphsPlot) + +For plots with multiple panels (e.g., a left annotation panel + right data panel): + +```r +plotObj <- jaspGraphs:::jaspGraphsPlot$new( + subplots = list(leftPanel, rightPanel), + layout = matrix(1:2, ncol = 2), + heights = 1, + widths = c(0.4, 0.6) +) +myPlot$plotObject <- plotObj +``` + +In tests, each subplot gets its own SVG snapshot: `"name-subplot-1"`, `"name-subplot-2"`. + +--- + +## 4) Per-Group Plot Pattern + +When a single fit produces a single plot, but multiple groups produce a container of plots: + +```r +if (options[["groupingVariable"]] == "") { + # Single plot, attach directly + plot <- .makePlotFun(fit[[1]], options) + plot$title <- gettext("My Plot") + plot$dependOn(dependencies) + jaspResults[["myPlot"]] <- plot + +} else { + # Container with one plot per group + container <- createJaspContainer() + container$title <- gettext("My Plot") + container$dependOn(dependencies) + jaspResults[["myPlot"]] <- container + + for (i in seq_along(fit)) { + container[[names(fit)[i]]] <- .makePlotFun(fit[[i]], options) + container[[names(fit)[i]]]$title <- gettextf("Group: %1$s", attr(fit[[i]], "group")) + container[[names(fit)[i]]]$position <- i + } +} +``` + +--- + +## 5) Separate-Plots-by-Variable Pattern + +When a variable creates multiple faceted plots: + +```r +if (length(options[["separatePlots"]]) > 0) { + container <- createJaspContainer() + for (i in seq_along(levels)) { + tempPlot <- createJaspPlot(title = levels[i], width = w, height = h) + tempPlot$position <- i + tempPlot$plotObject <- makePlot(data[data$facet == levels[i], ]) + container[[paste0("plot", i)]] <- tempPlot + } +} else { + plot <- createJaspPlot(width = w, height = h) + plot$plotObject <- makePlot(data) +} +``` diff --git a/.github/instructions/jasp-state-management.instructions.md b/.github/instructions/jasp-state-management.instructions.md new file mode 100644 index 0000000..1bb4f65 --- /dev/null +++ b/.github/instructions/jasp-state-management.instructions.md @@ -0,0 +1,257 @@ +--- +applyTo: "**/R/*.R" +description: "createJaspState caching, model fit patterns, metadata state, dynamic containers" +--- + +# JASP State Management (createJaspState) + +How to cache expensive computations and track dynamic output state. + +For the reactive loop context see [jasp-module-architecture.md](jasp-module-architecture.md). +For dependency mechanics see [jasp-dependency-management.md](jasp-dependency-management.md). + +Note that you cannot test this by running analysis via `runAnalysis(..., view = FALSE)` because you only generate one state at a time +(with no initial elements - ask the human maintainer to validate the dependencies manually if you suspect an issue!). + +--- + +## 1) Why State Objects Exist + +Model fitting is expensive. Without caching, every option change (even toggling a checkbox for an unrelated table) would re-run the computation. State objects solve this by caching results that persist across R invocations as long as their dependencies hold. + +```r +.computeModel <- function(jaspResults, dataset, options) { + if (!is.null(jaspResults[["modelFit"]])) + return() # cached → skip + + fitState <- createJaspState() + fitState$dependOn(.modelDeps) # only model options + jaspResults[["modelFit"]] <- fitState + + result <- try(expensiveFit(dataset, options)) + fitState$object <- result # cache +} +``` + +Now when the user toggles "Show CI" (a table option, not a model option), `jaspResults[["modelFit"]]` survives. Only when a model option changes does the fit get invalidated and recomputed. + +--- + +## 2) The $object Property + +`createJaspState()` stores arbitrary R objects via `$object`: + +```r +# Store anything: model fits, lists, data.frames +jaspResults[["modelFit"]]$object <- list(model = fitResult, residuals = resid) + +# Retrieve in another builder function +cached <- jaspResults[["modelFit"]]$object +if (is.null(cached)) return() # not yet computed +model <- cached$model +``` + +--- + +## 3) State vs Output Elements + +| | State | Table/Plot/Html | +|---|---|---| +| Visible to user | No | Yes | +| Has `$object` | Yes | No (use `$setData()`, `$plotObject`) | +| Purpose | Cache computations | Display results | +| `$dependOn()` | Yes | Yes | +| Can nest in container | Yes | Yes | + +--- + +## 4) Pattern: Model Fit Caching + +The most common pattern -- fit a model once, reuse across multiple tables and plots: + +```r +.computeModel <- function(jaspResults, dataset, options) { + if (!is.null(jaspResults[["modelFit"]])) + return() + + fitState <- createJaspState() + fitState$dependOn(.modelDeps) + jaspResults[["modelFit"]] <- fitState + + fit <- try(myPackage::fitModel( + formula = .buildFormula(options), + data = dataset + )) + + fitState$object <- fit +} + +# Used by multiple builders: +.extractFit <- function(jaspResults) { + cached <- jaspResults[["modelFit"]]$object + if (is.null(cached)) return(NULL) + return(cached) +} +``` + +--- + +## 5) Pattern: Multiple Fits (Per Group / Per Variable) + +When the analysis computes separate fits for groups or variables, store them as a named list: + +```r +.computeModel <- function(jaspResults, dataset, options) { + if (!is.null(jaspResults[["modelFit"]])) + return() + + fitState <- createJaspState() + fitState$dependOn(.modelDeps) + jaspResults[["modelFit"]] <- fitState + + results <- list() + + # Overall fit + results[["overall"]] <- try(fitFun(dataset, options)) + + # Per-group fits (if grouping variable selected) + if (options[["groupingVariable"]] != "") { + groups <- unique(dataset[[options[["groupingVariable"]]]]) + for (g in groups) { + subData <- dataset[dataset[[options[["groupingVariable"]]]] == g, ] + fit <- try(fitFun(subData, options)) + attr(fit, "group") <- as.character(g) # preserve metadata even on error + results[[paste0("group_", g)]] <- fit + } + } + + fitState$object <- results +} +``` + +**Key conventions:** +- Use `attr(fit, "group")` to tag each fit with its group label (survives `try()` errors) +- Extractors can filter: include/exclude overall, handle errors per group +- Row builders iterate over fits via `lapply()`, returning skeleton data.frames on error + +### Extractor with filtering + +```r +.extractFit <- function(jaspResults, options) { + results <- jaspResults[["modelFit"]]$object + if (is.null(results)) return(NULL) + + # Optionally exclude overall fit + if (options[["groupingVariable"]] != "" && !options[["includeOverall"]]) + results <- results[names(results) != "overall"] + + return(results) +} +``` + +--- + +## 6) Pattern: Shared Computation Cache + +When multiple output elements (table + plot) need the same intermediate result: + +```r +.computeDiagnostics <- function(jaspResults, options) { + if (!is.null(jaspResults[["diagnosticsCache"]])) + return(jaspResults[["diagnosticsCache"]]$object) + + state <- createJaspState() + state$dependOn(.diagnosticsDeps) + jaspResults[["diagnosticsCache"]] <- state + + results <- expensiveComputation(...) + state$object <- results + return(results) +} +``` + +Both `.diagnosticsTable()` and `.diagnosticsPlot()` call `.computeDiagnostics()` -- the second call returns the cached result immediately. + +--- + +## 7) Pattern: Metadata State for Dynamic Containers + +When the set of output children depends on user-selected variables, track what's currently rendered: + +```r +.buildVariableOutputs <- function(jaspResults, options) { + + container <- .extractContainer(jaspResults) + + # Get or create metadata state + if (!is.null(container[["metaData"]])) { + meta <- container[["metaData"]]$object + } else { + metaState <- createJaspState() + metaState$dependOn(c("selectedVariables")) + container[["metaData"]] <- metaState + meta <- list(existing = character(0)) + } + + selected <- options[["selectedVariables"]] + existing <- meta$existing + + # Remove deselected + for (v in setdiff(existing, selected)) + container[[v]] <- NULL + + # Add new + for (v in setdiff(selected, existing)) { + child <- createJaspContainer(title = v) + child$position <- which(selected == v) + container[[v]] <- child + .buildTableForVariable(child, jaspResults, options, v) + } + + # Update tracking + container[["metaData"]]$object <- list(existing = selected) +} +``` + +This avoids rebuilding the entire container when the user adds or removes a single variable. + +--- + +## 8) Pattern: Dataset Update Sentinel + +When an expensive fit should NOT be re-run for visualization-only option changes, but auxiliary data attached to the fit needs updating: + +```r +.updateFitData <- function(jaspResults, dataset, options) { + if (is.null(jaspResults[["modelFit"]])) + return() + if (!is.null(jaspResults[["fitDataUpdate"]])) + return() + + # Create sentinel with narrow deps + sentinel <- createJaspState() + sentinel$dependOn(.plottingVariableDeps) + jaspResults[["fitDataUpdate"]] <- sentinel + + # Update auxiliary data on the existing (cached) fit + fit <- jaspResults[["modelFit"]]$object + fit$plotData <- .prepPlotData(fit, dataset, options) + jaspResults[["modelFit"]]$object <- fit + + sentinel$object <- TRUE # mark as done +} +``` + +When a plotting variable changes: sentinel is NULLed, data is re-attached. The model fit itself survives. + +--- + +## 9) Common Pitfalls + +**Forgetting to store:** Creating a state but never assigning `$object` -- extractors see NULL. + +**Circular extraction:** An extractor that calls the compute function which calls the extractor. Use the `if (!is.null(...)) return()` guard pattern consistently. + +**Overwriting state from extractors:** Extractors should be read-only. Only the compute function should write to `$object`. + +**State without dependencies:** A state with no `$dependOn()` is never invalidated -- it persists forever with potentially stale data. diff --git a/.github/instructions/jasp-tables.instructions.md b/.github/instructions/jasp-tables.instructions.md new file mode 100644 index 0000000..3731ce0 --- /dev/null +++ b/.github/instructions/jasp-tables.instructions.md @@ -0,0 +1,202 @@ +--- +applyTo: "**/R/*.R" +description: "Table lifecycle, columns, rows, footnotes, error display in jaspResults" +--- + +# JASP Table Building Patterns + +How to create, configure, and populate tables in jaspResults. + +For dependency mechanics see [jasp-dependency-management.md](jasp-dependency-management.md). +For state/caching see [jasp-state-management.md](jasp-state-management.md). +For containers and error handling see [jasp-containers-and-errors.md](jasp-containers-and-errors.md). + +--- + +## 1) Complete Table Lifecycle + +```r +.myTable <- function(jaspResults, options) { + + container <- .myExtractContainer(jaspResults) + + # 1. SKIP if already created (idempotency) + if (!is.null(container[["myTable"]])) + return() + + fit <- .extractFit(jaspResults, options) + + # 2. CREATE table and attach to parent BEFORE filling data + myTable <- createJaspTable(gettext("My Table Title")) + myTable$position <- 1 + myTable$dependOn(c("optionA", "optionB")) + container[["myTable"]] <- myTable + + # 3. DEFINE columns + myTable$addColumnInfo(name = "term", type = "string", title = "") + myTable$addColumnInfo(name = "est", type = "number", title = gettext("Estimate")) + myTable$addColumnInfo(name = "se", type = "number", title = gettext("Standard Error")) + myTable$addColumnInfo(name = "pval", type = "pvalue", title = gettext("p")) + + # 4. EARLY RETURN on error (table shows as empty with error) + if (is.null(fit)) + return() + if (length(fit) == 1 && jaspBase::isTryError(fit[[1]])) { + myTable$setError(.cleanErrorMessage(fit[[1]])) + return() + } + + # 5. BUILD row data (list of data.frames → rbind) + rows <- do.call(rbind, lapply(fit, .myRowBuilder, options = options)) + + # 6. ADD footnotes + myTable$addFootnote(gettext("Some methodological note.")) + + # 7. SET data + myTable$setData(rows) +} +``` + +**Key**: Always attach the table to jaspResults (step 2) **before** checking errors (step 4). This ensures the empty table with error message displays rather than nothing. See [jasp-containers-and-errors.md](jasp-containers-and-errors.md) for the create-then-error pattern. + +--- + +## 2) Column Types + +| Type | Use for | Format examples | +|------|---------|-----------------| +| `"string"` | Labels, names, formatted test stats | -- | +| `"number"` | Numeric values | `"sf:4;dp:3"` (4 sig figs, 3 decimal places) | +| `"integer"` | Counts, df | -- | +| `"pvalue"` | p-values | `"dp:3;p:.001"` (3 dp, threshold at .001) | + +--- + +## 3) Column Modifiers + +```r +# Grouped column header (e.g., "95% CI" spanning Lower/Upper) +table$addColumnInfo(name = "lCi", type = "number", title = gettext("Lower"), + overtitle = gettextf("%s%% CI", 100 * options[["ciLevel"]])) + +# Show only explicitly added columns (hide data columns not in schema) +table$showSpecifiedColumnsOnly <- TRUE +``` + +--- + +## 4) DRY Pattern: Reusable Column Helpers + +When multiple tables share the same column groups (e.g., CI columns, SE columns, test statistics), factor out repeated `addColumnInfo()` calls into shared helper functions. For example, a helper that conditionally adds a CI lower/upper pair with a dynamic overtitle avoids duplicating those 3-4 lines across every table builder. + +Apply the same pattern for any column group that appears in more than one table — each helper takes the table and relevant options, and adds the columns conditionally. + +--- + +## 5) Parameterized Tables + +When the same table structure serves multiple purposes, parametrize the builder: + +```r +.myTable <- function(jaspResults, options, parameter = "main") { + + container <- .extractContainer(jaspResults) + tableKey <- paste0(parameter, "Table") + + if (!is.null(container[[tableKey]])) + return() + + table <- createJaspTable(switch(parameter, + main = gettext("Main Results"), + summary = gettext("Summary Results") + )) + table$position <- switch(parameter, main = 1, summary = 2) + container[[tableKey]] <- table + # ... columns and data +} +``` + +--- + +## 6) Row Builder Pattern + +Each row builder takes a **single fit** and returns a **data.frame** (one or more rows): + +```r +.myRowBuilder <- function(fit, options) { + + # Handle failed fits gracefully (return skeleton with NAs) + if (jaspBase::isTryError(fit)) { + return(data.frame( + term = gettext("My term"), + group = attr(fit, "group") + )) + } + + row <- data.frame( + term = gettext("My term"), + group = attr(fit, "group"), + est = fit$beta[1], + se = fit$se[1], + pval = fit$pval[1] + ) + + return(row) +} +``` + +**Key conventions:** +- Include `group = attr(fit, "group")` for per-group support +- On error, return data.frame with labels but missing numeric columns (renders as empty cells) +- Use `gettext()` / `gettextf()` for all user-visible strings + +--- + +## 7) DRY Pattern: Safe Data Aggregation + +When combining data.frames from multiple fits — especially when some fits may fail and return fewer columns — create a helper that: + +1. Filters out NULL/empty data.frames +2. Computes the union of all column names +3. Pads each data.frame with NA for missing columns +4. Calls `do.call(rbind, ...)` on the aligned data.frames + +This avoids `rbind()` failures when partial errors produce data.frames with heterogeneous columns. Apply the same helper pattern for ordering rows by grouping variable and simplifying output (e.g., dropping a grouping column when no groups are selected). + +--- + +## 8) Footnotes + +```r +# Simple footnote (appears at bottom) +table$addFootnote(gettext("Fixed effects tested using Knapp and Hartung adjustment.")) + +# Warning-style footnote +table$addFootnote(warningMsg, symbol = gettext("Warning:")) + +# Per-group error footnotes +for (i in which(sapply(fit, jaspBase::isTryError))) { + table$addFootnote( + gettextf("The model for group '%1$s' failed: %2$s", + attr(fit[[i]], "group"), .cleanError(fit[[i]])), + symbol = gettext("Error:") + ) +} + +# Cell-specific footnote +table$addFootnote(message, colNames = "est", rowNames = "rowLabel") +``` + +--- + +## 9) Error Display on Tables + +```r +# Error message replaces entire table content +table$setError(gettext("Feature not available for this model type.")) + +# Error from a try-error object +table$setError(.cleanErrorMessage(tryResult)) +``` + +See [jasp-containers-and-errors.md](jasp-containers-and-errors.md) for the full create-then-error and graceful degradation patterns. diff --git a/.github/instructions/testing.instructions.md b/.github/instructions/testing.instructions.md new file mode 100644 index 0000000..08953d8 --- /dev/null +++ b/.github/instructions/testing.instructions.md @@ -0,0 +1,164 @@ +--- +applyTo: "**/tests/testthat/*.R" +description: "Test framework, snapshots, and test workflow for JASP analyses" +--- + +# JASP Testing Instructions + +## 1) Test Framework + +This module uses the `jaspTools` testing framework. Tests are **critical** and must always pass before committing code. + +## 2) Running Tests + +Run via `btw_tool_run_r` in the persistent R session: + +**Agent-optimized** (preferred -- compact output, returns queryable result object): + +```r +# Full test suite -- returns rich S3 result object +x <- agentTestAll() + +# Specific analysis tests +x <- agentTestAnalysis("AnalysisName") +``` + +These return a `jaspAgentTestResults` object with fields: `$status`, `$summary`, `$failures`, `$warnings`, `$skips`, `$tests`, `$errorModules`, `$logFile`. + +**Human-oriented** (verbose output, for interactive use): + +```r +testAll() +testAnalysis("AnalysisName") +``` + +**Critical rules:** + +- Tests take 300+ seconds to complete +- **NEVER CANCEL** tests -- always let them run to completion +- Some deprecation warnings are expected and can be ignored +- ALL tests must pass before proceeding +- Some tests skip on certain platforms (e.g., Windows) -- this is expected +- Some stderr noise (ggplot messages, tryCatch errors) may leak through -- expected and minor + +## 3) Test File Structure + +Each test file in `tests/testthat/` corresponds to an R analysis file: + +- `test-penalizedmetaanalysis.R` -> `R/penalizedmetaanalysis.R` +- Test file name pattern: `test-.R` +- Analysis names for `agentTestAnalysis()` come from NAMESPACE exports (PascalCase) + +## 4) Writing Tests + +### Basic test structure + +```r +# 1. Set up analysis options +options <- jaspTools::analysisOptions("AnalysisName") +options$variables <- "contGamma" +options$descriptives <- TRUE + +# 2. Set seed for reproducibility +set.seed(1) + +# 3. Run the analysis +results <- jaspTools::runAnalysis("AnalysisName", "debug.csv", options, view = FALSE) + +# 4. Test tables +test_that("Table name matches", { + table <- results[["results"]][["tableName"]][["data"]] + jaspTools::expect_equal_tables(table, list(...expected values...)) +}) + +# 5. Test plots +test_that("Plot name matches", { + plotName <- results[["results"]][["containerName"]][["collection"]][["plotId"]][["data"]] + testPlot <- results[["state"]][["figures"]][[plotName]][["obj"]] + jaspTools::expect_equal_plots(testPlot, "plotname", dir = "AnalysisName") +}) +``` + +### Loading from .jasp example files + +```r +jaspFile <- testthat::test_path("..", "..", "examples", "Example Name.jasp") +opts <- jaspTools::analysisOptions(jaspFile) +dataset <- jaspTools::extractDatasetFromJASPFile(jaspFile) +encoded <- jaspTools:::encodeOptionsAndDataset(opts, dataset) +set.seed(1) +results <- jaspTools::runAnalysis("AnalysisName", encoded$dataset, encoded$options, encodedDataset = TRUE, view = FALSE) +``` + +### Key testing functions + +- `jaspTools::analysisOptions(name)` -- Get default options for an analysis +- `jaspTools::runAnalysis(name, dataset, options, view = FALSE)` -- Run analysis without generating HTML; inspect the returned R object +- `jaspTools::expect_equal_tables(actual, expected)` -- Compare table output +- `jaspTools::expect_equal_plots(plot, name, dir)` -- Compare plot output (snapshot-based) + +## 5) Test Data + +- `"debug.csv"` is a built-in jaspTools dataset containing most data types +- Use `set.seed()` before running analyses for reproducibility +- Example .jasp files in `examples/` provide pre-configured options and datasets + +## 6) Test Snapshots + +- Snapshots stored in `tests/testthat/_snaps/` +- **NEVER automatically accept snapshot changes** -- always notify user for manual inspection +- When a new snapshot is created, inform the user so they can verify it + +## 7) When to Update Tests + +### Always update tests when + +1. Adding new analysis outputs (tables, plots, text) +2. Modifying existing output structure or values +3. Adding new QML options that affect results +4. Changing analysis calculations + +### How to update test expectations + +1. Run tests and capture new output +2. Verify the new output is correct +3. Update expected values in test file +4. Re-run tests to confirm they pass + +## 8) Test Workflow + +### Before making code changes + +Run `agentTestAll()` via `btw_tool_run_r` to establish baseline -- all tests should pass. + +### After making code changes + +1. Run `devtools::load_all()` to hot-reload R changes +2. Run `agentTestAnalysis("AnalysisName")` for quick iteration on the affected analysis +3. Once the specific tests pass, run `agentTestAll()` to check for regressions + +### If tests fail + +1. Review the failure messages carefully +2. Check if failure is expected (due to your intentional changes) +3. If expected: update test expectations and notify user about snapshot changes +4. If unexpected: fix your code +5. Re-run tests until all pass + +## 9) Adding New Tests + +When adding a new analysis: + +1. Create test file: `tests/testthat/test-.R` +2. Set up options with all default values explicitly set +3. Test all output tables and plots +4. Test edge cases and error conditions +5. Use meaningful variable names and test data + +## 10) Best Practices + +- **One test per output element** -- separate `test_that()` blocks for each table/plot +- **Descriptive test names** -- clearly state what is being tested +- **Reproducible** -- always use `set.seed()` for analyses with randomness +- **Complete option coverage** -- test with various option combinations +- **Keep tests focused** -- each test should verify one specific aspect diff --git a/.github/instructions/translation.instructions.md b/.github/instructions/translation.instructions.md new file mode 100644 index 0000000..f8c7adb --- /dev/null +++ b/.github/instructions/translation.instructions.md @@ -0,0 +1,254 @@ +--- +applyTo: "**/R/*.R,**/inst/qml/*.qml,**/po/**" +description: "gettext/gettextf/qsTr usage, formatting, plurals, Weblate workflow" +--- + +# Translation (i18n) Instructions + +## 1) Core Principle + +**ALL user-visible text must be wrapped for translation.** + +This module is translated into multiple languages via Weblate integration. + +## 2) R Code Translation + +### Use `gettext()` for static strings: +```r +# Single string +message <- gettext("Analysis complete") + +# Table titles +tab <- createJaspTable(title = gettext("Descriptive Statistics")) + +# Error messages +tab$setError(gettext("Insufficient observations")) +``` + +### Use `gettextf()` for dynamic strings: +```r +# Single placeholder +msg <- gettextf("Variable %s has insufficient data", varName) + +# Multiple placeholders - use numbered format for translators +msg <- gettextf("Number of factor levels is %1$s in %2$s", nLevels, varName) + +# Percentage signs must be doubled +label <- gettextf("%s%% CI for Mean Difference", 100 * alpha) +``` + +### Use `ngettext()` for plurals: +```r +msg <- ngettext(n, + "One observation removed", + "%d observations removed", + domain = "R-jaspEquivalenceTTests") +``` + +### Column overtitles with dynamic content: +```r +if (options$confidenceInterval) { + ciLabel <- gettextf("%s%% CI", 100 * options$confidenceIntervalLevel) + tab$addColumnInfo("lower", gettext("Lower"), overtitle = ciLabel) + tab$addColumnInfo("upper", gettext("Upper"), overtitle = ciLabel) +} +``` + +## 3) QML Translation + +### Wrap all visible strings with `qsTr()`: +```qml +CheckBox +{ + name: "descriptives" + label: qsTr("Descriptive statistics") + + CheckBox + { + name: "confidenceInterval" + label: qsTr("Confidence interval") + info: qsTr("Display confidence intervals for effect sizes") + } +} +``` + +### For groups and sections: +```qml +Group +{ + title: qsTr("Additional Statistics") + + CheckBox + { + label: qsTr("Effect size") + } +} + +Section +{ + title: qsTr("Advanced Options") + + DoubleField + { + label: qsTr("Prior scale") + } +} +``` + +### Radio buttons and dropdowns: +```qml +RadioButtonGroup +{ + name: "hypothesis" + title: qsTr("Alternative Hypothesis") + + RadioButton + { + value: "twoSided" + label: qsTr("Two-sided") + } + + RadioButton + { + value: "greater" + label: qsTr("Greater than") + } +} + +DropDown +{ + name: "effectSize" + label: qsTr("Effect Size") + values: [ + { label: qsTr("Cohen's d"), value: "cohen" }, + { label: qsTr("Glass' delta"), value: "glass" } + ] +} +``` + +## 4) Translation Rules + +### DO wrap for translation: +- ✅ Table/plot/container titles +- ✅ Column names and overtitles +- ✅ Error messages and warnings +- ✅ Footnotes and citations +- ✅ All QML labels, titles, and info text +- ✅ Help text and descriptions +- ✅ Button labels and tooltips + +### DON'T wrap for translation: +- ❌ Empty strings: `""` (NEVER mark for translation) +- ❌ Variable names (internal identifiers) +- ❌ Statistical symbols: `"β"`, `"p"`, `"t"`, `"df"` +- ❌ Mathematical expressions +- ❌ Code or syntax +- ❌ File paths + +### Format specifications: +```r +# CORRECT - use numbered placeholders for clarity +gettextf("Mean difference is %1$s with SE = %2$s", mean, se) + +# AVOID - unnamed placeholders are harder for translators +gettextf("Mean difference is %s with SE = %s", mean, se) +``` + +### Special characters: +```r +# Use UTF-8 escape sequences for non-ASCII +label <- gettext("Cram\u00E9r's V") # Cramér's V +symbol <- gettext("\u03B2") # β (beta) +``` + +### Percentage signs in format strings: +```r +# WRONG - single % will cause format error +label <- gettextf("%s% CI", 95) + +# CORRECT - double %% in format string +label <- gettextf("%s%% CI", 95) +``` + +## 5) Translation Workflow + +### Automated process: +1. Developers write code with `gettext()`/`gettextf()`/`qsTr()` +2. Translation extraction happens automatically +3. Weblate platform provides translation interface +4. Translators work on Weblate +5. Translation files synced back to repository automatically +6. `.github/workflows/translations.yml` handles automation + +### Translation files location: +``` +po/ # R translation files +inst/qml/translations/ # QML translation files (if exists) +``` + +### Manual updates (rare): +Usually handled automatically, but if needed: +```bash +# Update R translations (done by translation workflow) +# Don't manually edit .po files unless absolutely necessary +``` + +## 6) Testing Translations + +While we can't easily test all languages locally, ensure: +1. All user-visible strings are wrapped +2. Format strings use numbered placeholders +3. Percentage signs are doubled in format strings +4. No empty strings marked for translation +5. Context provided for ambiguous terms + +## 7) Common Mistakes to Avoid + +### ❌ WRONG: +```r +# Missing translation +tab <- createJaspTable(title = "Descriptive Statistics") + +# Empty string marked for translation +label <- gettext("") + +# Unnamed placeholders +msg <- gettextf("Found %s issues in %s", count, name) + +# Single % for percentage +label <- gettextf("%s% Confidence Interval", 95) +``` + +### ✅ CORRECT: +```r +# Proper translation +tab <- createJaspTable(title = gettext("Descriptive Statistics")) + +# No translation for empty string +label <- "" + +# Numbered placeholders for translators +msg <- gettextf("Found %1$s issues in %2$s", count, name) + +# Doubled %% for percentage +label <- gettextf("%s%% Confidence Interval", 95) +``` + +## 8) Translation Context + +For ambiguous terms, consider adding comments: +```r +# "Mean" as in average (not "mean" as in unkind) +columnTitle <- gettext("Mean") + +# "Scale" as in measurement scale (not fish scales) +fieldLabel <- qsTr("Scale variable") +``` + +## 9) Weblate Integration + +- Weblate repo: `jaspequivalencettests-qml` and `jaspequivalencettests-r` +- Automated workflow: `.github/workflows/translations.yml` +- Scheduled runs: Weekly on Saturday at 2:45 AM +- Manual trigger: `workflow_dispatch` available +- Translation updates automatically create commits/PRs diff --git a/.gitignore b/.gitignore index 854a153..429fa44 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,8 @@ Thumbs.db .Rproj.user _processedLockFile.lock + +# AI agent config (machine-specific, not committed) +.mcp.json +.claude/settings.local.json +.vscode/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..cc38a43 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,286 @@ +# JASP Module + +ALWAYS follow these instructions first and fallback to additional search and context gathering ONLY if the information in these instructions is incomplete or found to be in error. + +This is a JASP module. It contains QML user-facing interfaces and R backend computations. + +In all interactions and commit messages, be extremely concise and sacrifice grammar for the sake of concision. + +## Detailed Instructions + +For comprehensive guidance on specific topics, read the corresponding rule file **before working on matching file types**: + +- **[Module Architecture](.codex/rules/jasp-module-architecture.md)** - **Start here.** QML-Desktop-R reactive loop, jaspResults persistence, options mapping, data flow. Read when working on `R/*.R`, `inst/qml/*.qml`, or `tests/testthat/*.R`. +- **[Dependency Management](.codex/rules/jasp-dependency-management.md)** - $dependOn mechanics, inheritance, vectors, per-value deps, sentinel pattern. Read when working on `R/*.R`. +- **[State Management](.codex/rules/jasp-state-management.md)** - createJaspState caching, model fit patterns, metadata state, dynamic containers. Read when working on `R/*.R`. +- **[R Backend Development](.codex/rules/r-instructions.md)** - R function structure, validation, style conventions. Read when working on `R/*.R`. +- **[Tables](.codex/rules/jasp-tables.md)** - Table lifecycle, columns, rows, footnotes, error display. Read when working on `R/*.R`. +- **[Plots](.codex/rules/jasp-plots.md)** - Plot lifecycle, composite plots, subgroup/facet patterns. Read when working on `R/*.R`. +- **[Containers & Errors](.codex/rules/jasp-containers-and-errors.md)** - Container patterns, HTML output, error handling. Read when working on `R/*.R`. +- **[QML Interface Development](.codex/rules/qml-instructions.md)** - QML controls, validation, bindings, and UI patterns. Read when working on `inst/qml/*.qml`. +- **[Testing & Test Writing](.codex/rules/testing-instructions.md)** - Test framework, snapshots, and test workflow. Read when working on `tests/testthat/*.R`. +- **[Translation (i18n)](.codex/rules/translation-instructions.md)** - gettext/gettextf/qsTr usage, formatting, plurals. Read when working on `R/*.R`, `inst/qml/*.qml`, or `po/`. +- **[Output Structure](.codex/rules/jasp-output-structure.md)** - Reading/testing serialized output (containers, tables, plots, state). Read when working on `tests/testthat/*.R` or `R/*.R`. +- **[Git Workflow](.codex/rules/git-workflow.md)** - Commit conventions, branch strategy, PR guidelines. Read before any git operations. + +## R Session via MCP + +This project uses the `btw` MCP server (`.claude/mcp-server.R`) to provide a persistent R session via `btw_tool_run_r`. The MCP server config is in `.codex/config.toml` (project-scoped) and should NOT be committed to git. + +**Session handoff:** The user sets up their R session (RStudio/Positron/radian), runs `btw::btw_mcp_session()`, and hands it over. Connect via `list_r_sessions` / `select_r_session`. All `btw_tool_run_r` calls then execute in the user's session with full access to loaded packages and objects. The following R packages are required for the mcp server: `btw`, `mcptools`. + +### Available MCP Tools + +Use these R-specific tools instead of shell commands when possible: + +| Tool | Use for | +|------|---------| +| `btw_tool_run_r` | Execute R code in persistent session (variables persist between calls) | +| `btw_tool_docs_help_page` | Look up R function documentation | +| `btw_tool_docs_package_news` | Check package changelogs | +| `btw_tool_docs_available_vignettes` | Find package vignettes | +| `btw_tool_env_describe_environment` | Inspect objects in the R session | +| `btw_tool_env_describe_data_frame` | Inspect data frame structure | +| `btw_tool_search_packages` | Search CRAN for packages | +| `btw_tool_session_platform_info` | Check R version and platform | +| `btw_tool_session_check_package_installed` | Verify package availability | + +**Use Codex native tools** (shell, file read/write, apply_patch, search) for file editing, git operations, and file search -- they are faster than MCP equivalents. + +## Working Effectively + +### Session Setup (done by user) + +At the start of a session, check for a connected R session via `list_r_sessions`. If none is available, **prompt the user** to run in their interactive R console: + +```r +source(".claude/session_startup.R") +``` + +This restores dependencies, installs the module, configures jaspTools, and registers the session. Then connect via `list_r_sessions` / `select_r_session`. + +### Hot-Reload After Code Changes + +- **R code only changed:** `devtools::load_all()` via `btw_tool_run_r` +- **QML, dependencies, or imports changed:** `renv::install(".", prompt = FALSE)` + +### Running Tests + +Run via `btw_tool_run_r` in the persistent session: + +**Agent-optimized** (preferred -- compact output, returns queryable result object): + +```r +# Full test suite -- returns rich S3 result object +x <- agentTestAll() + +# Specific analysis tests +x <- agentTestAnalysis("AnalysisName") +``` + +These return a `jaspAgentTestResults` object. Console output is a compact one-line summary: +``` +== Test Results == FAIL: 0 | WARN: 0 | SKIP: 2 | PASS: 72 | Time: 3.6s +``` + +Query the result object directly: +```r +x$status # 0 = all passed, 1 = failures +x$summary # list(fail, warn, skip, pass, time) +x$failures # data.frame: module | file | test | message +x$warnings # data.frame: module | file | test | message +x$skips # data.frame: module | file | test | reason +x$tests # data.frame: all tests with module | file | context | test | passed | failed | ... +x$errorModules # named character vector of module-level errors +x$logFile # path to detailed JSON log (with backtraces) +``` + +**Human-oriented** (verbose output, for interactive use): +```r +testAll() +testAnalysis("AnalysisName") +``` + +**Rules:** +- Tests take 300+ seconds to complete -- **NEVER CANCEL** +- Run `agentTestAll()` at session start to verify baseline, and after all fixes +- Use `agentTestAnalysis("Name")` for quick iteration on specific analyses +- Analysis names are PascalCase exports from NAMESPACE +- Some tests skip on certain platforms (e.g., Windows) -- expected +- Some stderr noise (ggplot messages, tryCatch errors) may leak through -- expected and minor +- **MCP timeout:** If `btw_tool_run_r` times out on `agentTestAll()`, do NOT retry -- use the Bash fallback in [testing-instructions.md](.codex/rules/testing-instructions.md) + +**See [testing-instructions.md](.codex/rules/testing-instructions.md) for detailed test writing guidelines, snapshots, and workflows.** + +### Running a Specific Analysis + +**With built-in debug dataset:** +```r +options <- jaspTools::analysisOptions("AnalysisName") +options$someOption <- value +set.seed(1) +results <- jaspTools::runAnalysis("AnalysisName", "debug.csv", options, view = FALSE) +``` + +**From a .jasp example file:** +```r +jaspFile <- file.path("examples", "Example Name.jasp") +opts <- jaspTools::analysisOptions(jaspFile) +dataset <- jaspTools::extractDatasetFromJASPFile(jaspFile) +encoded <- jaspTools:::encodeOptionsAndDataset(opts, dataset) +set.seed(1) +results <- jaspTools::runAnalysis("AnalysisName", encoded$dataset, encoded$options, encodedDataset = TRUE, view = FALSE) +``` + +The encoding step is required because JASP internally encodes variable names and options to resolve ambiguities (e.g., same variable used with different types). + +**From a user-provided .jasp file:** Use the same pattern above. This is the primary way to reproduce bugs reported by users. + +**NEVER instantiate jaspResults C++ objects directly** (e.g., `jaspResultsClass$new()`, `create_cpp_jaspResults()`, `jaspBase:::initJaspResults()`). These require JASP Desktop C++ initialization unavailable in headless R sessions. They crash with `Rcpp::not_initialized` or `Expecting an external pointer`. Always use `jaspTools::runAnalysis()` or `agentTestAll()` which handle initialization internally. + +### Inspecting Results + +Always set `view = FALSE` when running `runAnalysis()` manually. This avoids HTML generation; inspect the returned R object instead. + +After `runAnalysis()`, check: +- `results$status` -- `"complete"` or `"fatalError"` +- `results$results` -- nested list of output containers, tables, plots +- `results$results$errorMessage` -- if status is fatalError + +### Finding Analysis Names + +1. Check roxygen documentation in R files (if available) +2. Parse `NAMESPACE` for `export()` directives + +### Test Snapshots + +- Snapshots stored in `tests/testthat/_snaps/` +- **NEVER automatically accept snapshot changes** -- always notify user for manual inspection +- When a snapshot is newly created, inform the user + +### Repository Structure +``` +/ +├── R/ # Backend R analysis functions +├── inst/ +│ ├── qml/ # QML interface definitions +│ ├── Descriptions/ # Analysis descriptions (Description.qml) +│ ├── help/ # Markdown help files +│ └── Upgrades.qml # Version upgrade mappings +├── examples/ # Example .jasp files for testing +├── tests/testthat/ # Unit tests using jaspTools +├── .codex/ # Codex CLI instructions and config +│ ├── config.toml # MCP servers, sandbox, approval settings +│ ├── rules/ # Rule files (referenced from AGENTS.md) +│ └── README.md # Codex CLI setup documentation +├── .agents/ # Cross-platform agent skills +│ └── skills/ # Skills (debugging, etc.) +├── .claude/ # Claude Code instructions and MCP server +│ ├── mcp-server.R # MCP server startup script (shared) +│ └── session_startup.R # R session bootstrap (shared) +├── .github/workflows/ # CI/CD automation +├── DESCRIPTION # R package metadata +├── NAMESPACE # Exported analysis names +└── renv.lock # R dependency lockfile +``` + +### Key Files to Check After Changes +- Always check corresponding test file in `tests/testthat/` when modifying R functions +- For released analyses, update `inst/Upgrades.qml` when renaming QML options to maintain backward compatibility. For unreleased analyses, keep only the current QML/R names. + +## Critical Safety Rules + +- **NEVER directly edit test files** under `tests/`. Test files are human-owned. Fix source code instead. +- **NEVER automatically accept snapshot changes** -- always notify user for manual inspection. +- **NEVER push/create PRs/merge without explicit human approval.** +- **NEVER use `library()` or `require()`** in R code -- use `package::function()` syntax. +- **NEVER cancel running tests** -- they take 300+ seconds, always let them complete. + +## Development Rules + +### Dependencies +- Avoid new dependencies -- re-implement simple functions instead of importing a whole package +- If a new dependency is truly needed, add it to DESCRIPTION and update renv.lock + +### QML Interface Rules +- QML interfaces in `inst/qml/` define user-facing options passed to R functions +- Each analysis links: `inst/Description.qml/` -> `inst/qml/` -> `R/` functions +- QML elements use `name` (camelCase internal) and `title`/`label` (user-facing) +- QML `name` values are the exact R option API. When changing option names, update R reads and dependency vectors to the current names; do not keep old aliases for unreleased analyses. +- Keep maintainable imported QML components when they reduce duplication/clutter. Do not inline or flatten QML solely because `jaspTools::analysisOptions()` cannot see imported components or dynamic bindings. +- Verify QML/R option contracts with source-aware checks across the main QML file and imported components; when tests need defaults tooling cannot derive, pass explicit GUI-equivalent options instead of adding R defaults. +- Preserve dynamic `DropDown.values` when they are the clearer GUI. Use static values plus `enabledOptions` only when that is the intended UX, not as a tooling workaround. +- Document QML elements using `info` property for help generation +- Use existing QML files as examples for structure and style +- Add default values to unit tests when adding new QML options + +**See [qml-instructions.md](.codex/rules/qml-instructions.md) for comprehensive QML controls reference, validation patterns, and UI conventions.** + +### R Backend Rules +- R functions in `R/` directory called by analyses in `inst/Descriptions/` +- Use camelCase for all function and variable names +- NEVER use `library()` or `require()` - use `package::function()` syntax +- Access `options` list via `options[["name"]]` notation to avoid partial matching +- Treat GUI options as a strict contract: do not add R-side normalization, alias maps, compatibility layers, or backup defaults for missing QML options in unreleased work. Missing/disconnected options should fail so the QML/R mismatch is fixed. +- For checkbox options, use `if (options[["flag"]])`, not `isTRUE(options[["flag"]])`; `isTRUE()` masks missing options. +- Follow CRAN guidelines for code structure and documentation + +**See [r-instructions.md](.codex/rules/r-instructions.md) for complete R function structure, jaspResults API, output components (tables/plots/containers/state), and coding conventions.** + +### Input Validation and Error Handling +- **TARGETED VALIDATION ONLY**: Since `options` are validated in the GUI, R functions should NOT check user input validity except for specific cases +- **VALIDATE ONLY**: `dataset` object (data.frame from GUI), `TextField` options, and `FormulaField` options (arbitrary text input) +- Use `gettext()` and `gettextf()` for all user-visible messages (internationalization) +- For `dataset` validation, check: missing values, infinity, negative values, insufficient observations, factor levels, variance +- Example: `.hasErrors(dataset, type = c('observations', 'variance', 'infinity'), all.target = options$variables, observations.amount = '< 3', exitAnalysisIfErrors = TRUE)` +- Validate dataset assumptions automatically when required for analysis validity +- Use footnotes for assumption violations that affect specific cells/values +- Place critical errors that invalidate entire analysis over the results table + +### Error Message Guidelines +- Write clear, actionable error messages that prevent user confusion +- Use `gettextf()` with placeholders for dynamic content: `gettextf("Number of factor levels is %1$s in %2$s", levels, variable)` +- For multiple arguments, use `%1$s`, `%2$s` format for translator clarity +- Use `ngettext()` for singular/plural forms +- Never mark empty strings for translation +- Use UTF-8 encoding for non-ASCII characters: `\u03B2` for beta +- Double `%` characters in format strings: `gettextf("%s%% CI for Mean")` + +**See [translation-instructions.md](.codex/rules/translation-instructions.md) for comprehensive i18n guidelines including QML qsTr(), R gettext/gettextf/ngettext, formatting rules, and Weblate workflow.** + +## CI/CD Pipeline +- GitHub Actions in `.github/workflows/unittests.yml` runs on every push +- Triggers on changes to R, test, or package files +- Uses jasp-stats/jasp-actions reusable workflow + +## Git Workflow + +- **ALWAYS work on feature branches** -- never commit directly to `master` +- **NEVER push/create PRs/merge without explicit human approval** +- Commit locally freely, but wait for approval before pushing to remote + +## Common Tasks + +### Adding New Analysis + +1. Create R function in `R/` directory following camelCase naming +2. Add QML interface in `inst/qml/` +3. Define analysis in `inst/Description.qml` +4. Add unit tests in `tests/testthat/` +5. Run `agentTestAll()` to validate (300+ seconds, NEVER CANCEL) + +### Modifying Existing Analysis + +1. Update R function maintaining existing interface +2. Update QML if adding/changing options +3. Update unit tests and expected results +4. Add upgrade mapping to `inst/Upgrades.qml` if renaming options for a released analysis +5. Run tests: `agentTestAll()` (NEVER CANCEL, 300+ seconds) + +### Detailed Development Process +- **Step 1**: Create main analysis function with `jaspResults`, `dataset`, `options` arguments +- **Step 2**: **CRITICAL** - Use `.quitAnalysis()` for `dataset`, `TextField`, `FormulaField` validation only +- **Step 3**: Create output tables/plots with proper dependencies, citations, column specs +- 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 From bafe0cd505f39945efedc2b53c6504d5d26ea878 Mon Sep 17 00:00:00 2001 From: Julius Pfadt Date: Tue, 18 Aug 2026 15:54:18 +0200 Subject: [PATCH 10/14] i18n: title case Prior Distributions and Advanced Options sections Co-Authored-By: Claude Opus 5 (1M context) --- inst/qml/bayesianProcessCapabilityStudies.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/inst/qml/bayesianProcessCapabilityStudies.qml b/inst/qml/bayesianProcessCapabilityStudies.qml index 8bfa4f9..7623d13 100644 --- a/inst/qml/bayesianProcessCapabilityStudies.qml +++ b/inst/qml/bayesianProcessCapabilityStudies.qml @@ -358,7 +358,7 @@ Form Section { - title: qsTr("Prior distributions") + title: qsTr("Prior Distributions") // TODO: this dropdown should just show the same GUI as the custom one // but disable e.g., the DropDown itself and instead show the prior @@ -425,7 +425,7 @@ Form Section { - title: qsTr("Advanced options") + title: qsTr("Advanced Options") Group { From 032a6046f83c14991b6c869a8f87de06ee4153c3 Mon Sep 17 00:00:00 2001 From: Julius Pfadt Date: Tue, 18 Aug 2026 15:55:24 +0200 Subject: [PATCH 11/14] refactor: capitalize BayesianProcessCapabilityStudies.qml Add explicit qml: entry to Description.qml since the filename no longer matches func:. Co-Authored-By: Claude Opus 5 (1M context) --- inst/Description.qml | 1 + ...apabilityStudies.qml => BayesianProcessCapabilityStudies.qml} | 0 2 files changed, 1 insertion(+) rename inst/qml/{bayesianProcessCapabilityStudies.qml => BayesianProcessCapabilityStudies.qml} (100%) diff --git a/inst/Description.qml b/inst/Description.qml index af7bf6b..eaef1eb 100644 --- a/inst/Description.qml +++ b/inst/Description.qml @@ -24,6 +24,7 @@ Description Analysis { title: qsTr("Bayesian Process Capability Study") + qml: "BayesianProcessCapabilityStudies.qml" func: "bayesianProcessCapabilityStudies" preloadData: true } diff --git a/inst/qml/bayesianProcessCapabilityStudies.qml b/inst/qml/BayesianProcessCapabilityStudies.qml similarity index 100% rename from inst/qml/bayesianProcessCapabilityStudies.qml rename to inst/qml/BayesianProcessCapabilityStudies.qml From c2dc0e205d0ed41722ee0e3f8abd64b7133a6014 Mon Sep 17 00:00:00 2001 From: Julius Pfadt Date: Tue, 18 Aug 2026 16:00:12 +0200 Subject: [PATCH 12/14] feat: keep spec limit and target fields editable when unchecked Lets users enter values before enabling the corresponding checkbox. Co-Authored-By: Claude Opus 5 (1M context) --- inst/qml/BayesianProcessCapabilityStudies.qml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/inst/qml/BayesianProcessCapabilityStudies.qml b/inst/qml/BayesianProcessCapabilityStudies.qml index 7623d13..8c37259 100644 --- a/inst/qml/BayesianProcessCapabilityStudies.qml +++ b/inst/qml/BayesianProcessCapabilityStudies.qml @@ -135,6 +135,7 @@ Form label: qsTr("Lower specification limit") id: lowerSpecificationLimit childrenOnSameRow: true + enableChildrenOnChecked: false DoubleField { @@ -153,6 +154,7 @@ Form label: qsTr("Target value") id: target childrenOnSameRow: true + enableChildrenOnChecked: false DoubleField { @@ -170,6 +172,7 @@ Form label: qsTr("Upper specification limit") id: upperSpecificationLimit childrenOnSameRow: true + enableChildrenOnChecked: false DoubleField { From 662682b79a376d2335e36e6010dd763642946568 Mon Sep 17 00:00:00 2001 From: Julius Pfadt Date: Tue, 18 Aug 2026 16:06:42 +0200 Subject: [PATCH 13/14] perf: skip prior sampling until spec limits are set .bpcsCanSampleFromPriors() is TRUE by default, so the prior MCMC ran on every option change and was discarded by the readiness guards downstream. Co-Authored-By: Claude Opus 5 (1M context) --- R/bayesianProcessCapabilityStudies.R | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/R/bayesianProcessCapabilityStudies.R b/R/bayesianProcessCapabilityStudies.R index 3ae175b..8060a17 100644 --- a/R/bayesianProcessCapabilityStudies.R +++ b/R/bayesianProcessCapabilityStudies.R @@ -22,7 +22,9 @@ bayesianProcessCapabilityStudies <- function(jaspResults, dataset, options) { fit <- .bpcsCapabilityTable(jaspResults, dataset, options, position = 1) - priorFit <- .bpcsSamplePosteriorOrPrior(jaspResults, dataset, options, prior = TRUE) + # drawing prior samples is pointless until the spec limits are set, and every + # consumer of priorFit already handles NULL + priorFit <- if (.bpcsIsReady(options)) .bpcsSamplePosteriorOrPrior(jaspResults, dataset, options, prior = TRUE) else NULL .bpcsCapabilityPlot(jaspResults, options, fit, priorFit, position = 2) .bpcsCapabilityPlot(jaspResults, options, fit, priorFit, position = 3, base = "priorDistributionPlot") From 7bf04d3fce33a0a64087ad76b124a52df8c4a193 Mon Sep 17 00:00:00 2001 From: Julius Pfadt Date: Wed, 19 Aug 2026 15:58:24 +0200 Subject: [PATCH 14/14] fix: scale posterior/prior plot annotation text and correct CI level units qc::plot_density()'s point-estimate/CI label defaults to an 18mm textsize, which overruns the panel; scale it to the actual panel width instead. Also convert IndividualCiMass (a 1-100 percentage field) to the 0-1 proportion qc::plot_density() expects for ci_level, which previously errored whenever CI was enabled on the posterior/prior distribution plot. Co-Authored-By: Claude Sonnet 5 --- R/bayesianProcessCapabilityStudies.R | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/R/bayesianProcessCapabilityStudies.R b/R/bayesianProcessCapabilityStudies.R index 8060a17..8e56a72 100644 --- a/R/bayesianProcessCapabilityStudies.R +++ b/R/bayesianProcessCapabilityStudies.R @@ -390,9 +390,11 @@ bayesianProcessCapabilityStudies <- function(jaspResults, dataset, options) { # only if the user asked for it priorSummaryObject <- if (isPost && options[[paste0(base, "PriorDistribution")]]) priorFit$summaryObject else NULL + plotWidth <- 400 * (if (singlePanel) 1 else 3) + jaspPlt <- createJaspPlot( title = if (isPost) gettext("Posterior Distribution") else gettext("Prior Distribution"), - width = 400 * (if (singlePanel) 1 else 3), + width = plotWidth, height = 400 * (if (singlePanel) 1 else 2), position = position, dependencies = jaspDeps( @@ -424,19 +426,32 @@ bayesianProcessCapabilityStudies <- function(jaspResults, dataset, options) { NULL } else { + # qc draws the point estimate / ci annotation with ggtext::geom_richtext(size = ...), + # whose size is in mm and defaults to 18 (~51pt), swamping the panel. Scale it to the + # width one panel actually gets instead: facet_wrap spreads the metrics over + # ceiling(sqrt(n)) columns of plotWidth, and 3mm is the largest that keeps the longest + # label ("Mean = x.xxx; xx.x% CI [x.xxx, x.xxx]") inside a 400px panel. Never grow past + # the standard jasp font size. + nColumns <- if (singlePanel) 1L else ceiling(sqrt(length(selectedMetrics))) + panelWidth <- plotWidth / nColumns + annotationSize <- min(3 * panelWidth / 400, jaspGraphs::graphOptions("fontsize") / ggplot2::.pt) + jaspPlt$plotObject <- qc::plot_density( summaryObject, what = selectedMetrics, point_estimate = if (options[[paste0(base, "IndividualPointEstimate")]]) options[[paste0(base, "IndividualPointEstimateType")]] else "none", ci = if (options[[paste0(base, "IndividualCi")]]) options[[paste0(base, "IndividualCiType")]] else "none", - ci_level = options[[paste0(base, "IndividualCiMass")]], + # IndividualCiMass is a 1-100 percentage (see Common/PlotLayout.qml's CIField overrides), + # but qc::plot_density's ci_level wants a 0-1 proportion and errors above 1. + ci_level = options[[paste0(base, "IndividualCiMass")]] / 100, ci_custom_left = options[[paste0(base, "IndividualCiLower")]], ci_custom_right = options[[paste0(base, "IndividualCiUpper")]], bf_support = options[[paste0(base, "IndividualCiBf")]], single_panel = singlePanel, axes = options[[paste0(base, "Axes")]], axes_custom = .bpcsGetCustomAxisLimits(options, base), - priorSummaryObject = priorSummaryObject + priorSummaryObject = priorSummaryObject, + textsize = annotationSize ) + jaspGraphs::geom_rangeframe() + jaspGraphs::themeJaspRaw()