PEBL 2.2
Psychology Experiment Building Language - Cross-platform psychological experiment development system
ScaleDefinition.h
Go to the documentation of this file.
1// ScaleDefinition.h - PEBL Scale data model (ScaleRunner format)
2// Copyright (c) 2026 Shane T. Mueller
3// Licensed under GPL
4
5#ifndef SCALE_DEFINITION_H
6#define SCALE_DEFINITION_H
7
8#include <string>
9#include <vector>
10#include <map>
11#include <memory>
12#include <json.hpp>
13
14// Scale metadata (matches ScaleRunner scale_info)
15struct ScaleInfo {
16 std::string name; // "Grit Scale"
17 std::string code; // "grit"
18 std::string abbreviation; // "Grit-O"
19 std::string description; // Brief description
20 std::string citation; // Full citation with DOI
21 std::string license; // Short license label (e.g., "CC BY 4.0", "Public Domain")
22 std::string license_explanation; // Full license terms / permissions grant
23 std::string license_url; // URL documenting the license terms
24 std::string version; // "1.0"
25 std::string url; // Website URL
26 std::string domain; // Subject domain (e.g., "Mood", "Substance Use", "Personality")
27
28 ScaleInfo() : version("1.0") {}
29};
30
31// Runtime parameter definition
33 std::string type; // "boolean", "integer", "string", "choice"
34 std::string defaultValue; // Default value as string
35 std::string description; // Description
36 std::vector<std::string> options; // Allowed values (for choice/boolean types)
37
38 ScaleParameter() = default;
39 ScaleParameter(const std::string& t, const std::string& d, const std::string& desc)
40 : type(t), defaultValue(d), description(desc) {}
41};
42
43// Likert options (global for scale)
45 int points; // Number of points (e.g., 5)
46 int min; // Minimum value (optional, defaults based on points)
47 int max; // Maximum value (optional, defaults based on points)
48 std::string question_head; // Translation key for instructions
49 std::vector<std::string> labels; // Translation keys for each option
50
51 LikertOptions() : points(5), min(-1), max(-1), question_head("question_head") {}
52 // Note: min=-1, max=-1 means "use defaults based on points"
53};
54
55// Visible-when condition (for conditional display)
57 std::string source_type; // "parameter" or "question"
58 std::string source_name; // parameter name or question ID
59 std::string op; // "equals", "not_equals", "greater_than", "less_than", "in", "not_in"
60 std::string value; // scalar value (when is_list == false)
61 std::vector<std::string> values; // list value (when is_list == true, for "in"/"not_in")
62 bool is_list; // true when value field is a JSON array
63 VisibleWhenCondition() : source_type("parameter"), op("equals"), is_list(false) {}
64};
65
66// Dimension definition (for scoring and organization)
68 std::string id; // "COI" (used in scoring)
69 std::string name; // "Consistency of Interest"
70 std::string abbreviation; // "COI"
71 std::string description; // Full description
72 std::string enabled_param; // Parameter name to enable/disable (or empty)
73 bool selectable; // If true, auto-generate a boolean enable param in schema
74 bool default_enabled; // Default value for the auto-generated enable param
75
76 // Dynamic visibility conditions
78 std::string visible_when_logic; // "all" or "any"
79 std::vector<VisibleWhenCondition> visible_when;
80
83 ScaleDimension(const std::string& i, const std::string& n, const std::string& abbr)
84 : id(i), name(n), abbreviation(abbr), has_visible_when(false), visible_when_logic("all") {}
85};
86
87// Per-question input validation — each constraint is independent with its own error key.
88// Integer constraints use -1 to mean "not set". Boolean flags gate the number constraints.
90 // Character length (short / long)
91 int min_length; // -1 = not set
92 int max_length; // -1 = not set
93 std::string min_length_error; // translation key (auto-generated from question id)
94 std::string max_length_error;
95
96 // Word count (short / long)
99 std::string min_words_error;
100 std::string max_words_error;
101
102 // Numeric range (short — implies number-only keyboard)
107 std::string number_min_error;
108 std::string number_max_error;
109
110 // Regex pattern (short)
111 std::string pattern; // empty = not set
112 std::string pattern_error;
113
114 // Selection count (multicheck)
119
120 bool HasAnyValidation() const {
121 return min_length >= 0 || max_length >= 0 ||
122 min_words >= 0 || max_words >= 0 ||
124 !pattern.empty() ||
125 min_selected >= 0 || max_selected >= 0;
126 }
127
132};
133
134// Question definition (structure only - text comes from translations)
136 std::string id; // "grit2"
137 std::string text_key; // Translation key (usually same as id)
138 std::string type; // inst, likert, vas, grid, short, long, multi, multicheck, image, imageresponse, section
139 std::string dimension; // Dimension id (or empty/null)
140 int coding; // 1 = normal, -1 = reverse, 0 = not scored
141 int random_group; // 0 = fixed position, >0 = shuffle group ID
142 int required_state; // -1 = use default, 0 = optional, 1 = required
143 QuestionValidation validation; // Input validation rules (C9)
144
145 // Conditional display
147 std::string visible_when_logic; // "all" or "any"
148 std::vector<VisibleWhenCondition> visible_when_simple; // flat conditions (UI-editable)
149 bool visible_when_is_complex; // nested beyond flat — raw JSON only
150
151 // Type-specific fields
152 int likert_points; // For likert questions
153 int likert_min; // For likert questions (optional, -1 = use scale default)
154 int likert_max; // For likert questions (optional, -1 = use scale default)
155 bool likert_reverse; // Display buttons right-to-left (highest value on left)
156 bool randomize_options; // Shuffle option order for multi/multicheck (S5)
157 std::vector<std::string> likert_labels; // For likert questions (optional, empty = use scale default)
158 int min_value; // For vas
159 int max_value; // For vas
160 std::string left_label; // Translation key for VAS left (min_label)
161 std::string right_label; // Translation key for VAS right (max_label)
162 std::string vas_orientation; // "horizontal" (default) or "vertical"
163 struct VasAnchor {
164 double value;
165 std::string label; // Translation key
166 };
167 std::vector<VasAnchor> vas_anchors; // Named anchor points along the scale
168 std::vector<std::string> options; // Translation keys for multi/multicheck
169 std::vector<std::string> correct; // Correct answer keys for multi
170 std::string image; // Image path for image/imageresponse
171 std::vector<std::string> columns; // Translation keys for grid columns
172 std::vector<std::string> rows; // Translation keys for grid rows
173
174 // Per-item question head — overrides scale-level likert_options.question_head
175 std::string question_head; // Translation key (empty = use scale default)
176
177 // Answer alias — optional semantic name for use in {answer.alias} piping
178 std::string answer_alias;
179
180 // Gate (blocking) — if set, a wrong answer stops the scale, saves data, and shows a message
182 std::string gate_required_value; // Exact-match form (multi): value that allows continuation
183 std::string gate_operator; // Numeric form (short): "greater_than","less_than","equals","not_equals"
184 double gate_value; // Numeric form (short): threshold to compare against
185 std::string gate_terminate_message_key; // Translation key for the termination message
186
187 // Section control (type == "section" only)
188 bool revisable; // Default true: runners allow Back button within this section. false = responses are final.
189 bool section_randomize; // Default false: shuffle non-fixed questions within section
190 std::vector<std::string> section_randomize_fixed; // Question IDs pinned to original position
191
193};
194
196 double min;
197 double max;
198 std::string label;
199 NormThreshold() : min(0), max(0) {}
200 NormThreshold(double mn, double mx, const std::string& lbl) : min(mn), max(mx), label(lbl) {}
201};
202
203// A single transform step: op (add/subtract/multiply/divide) + numeric value
205 std::string op; // "add", "subtract", "multiply", "divide"
206 double value; // Numeric operand
207
208 TransformStep() : op("add"), value(0.0) {}
209 TransformStep(const std::string& o, double v) : op(o), value(v) {}
210};
211
212// Scoring definition for a dimension
214 std::string method; // mean_coded, sum_coded, weighted_sum, weighted_mean, sum_correct
215 std::vector<std::string> items; // Question IDs in this score (used internally; serialized as dual-format items)
216 std::vector<std::string> scores; // Score IDs used as inputs (for composite scores)
217 std::string description; // Description of the score
218 std::map<std::string, double> weights; // For weighted_sum (optional)
219 std::map<std::string, int> item_coding; // Per-item coding: item_id -> 1 (normal), -1 (reverse), 0 (not scored). Serialized into items field.
220 std::map<std::string, std::vector<std::string>> correct_answers; // For sum_correct: item_id -> list of acceptable answers/patterns
221 std::vector<NormThreshold> norms; // Interpretation thresholds for report (optional)
222 std::vector<TransformStep> transform; // Post-scoring arithmetic transform steps (optional)
223 std::map<std::string, std::vector<double>> value_map; // Per-item response remapping: item_id (or "default") -> array[i] = recoded value for response min+i
224
225 DimensionScoring() : method("mean_coded") {}
226};
227
228// Report configuration
230 std::string template_type; // "standard", etc.
231 std::vector<std::string> include; // ["timestamp", "completion_time", "dimension_scores"]
232 std::string header; // Header text
233 std::vector<std::string> footer_refs; // Footer references/citations
234
235 ReportConfig() : template_type("standard") {}
236};
237
238// Data output configuration
240 std::string individual_file; // "grit-{subnum}.csv"
241 std::string pooled_file; // "grit-pooled.csv"
242 std::string report_file; // "grit-report-{subnum}.html"
243 std::string columns; // CSV column specification
244 std::string pooled_columns; // Pooled file columns
245};
246
247// Computed variable definition (S7)
249 std::string expression; // e.g., "score.PHQ_total >= 10" or "answer.q1 * answer.q2 * 8.0"
250 std::string type; // "boolean", "number"
251 std::vector<NormThreshold> norms; // Optional interpretation thresholds
252
253 ComputedVariable() : type("number") {}
254};
255
256// Translation data (language -> key -> value)
257using ScaleTranslations = std::map<std::string, std::map<std::string, std::string>>;
258
259// Main scale definition class
261public:
264
265 // Factory methods
266 static std::shared_ptr<ScaleDefinition> CreateNew(const std::string& code);
267 static std::shared_ptr<ScaleDefinition> LoadFromFile(const std::string& jsonPath);
268
269 // Serialization
270 bool SaveToFile(const std::string& jsonPath);
271 bool ExportToJSON(const std::string& definitionsPath, const std::string& translationsPath);
272 bool ExportToOSD(const std::string& outputDir); // Bundle definition + all translations into {CODE}.osd
273
274 // Load definition and translations from a scales directory (<basePath>/<code>/<code>.json)
275 bool LoadFromScalesDir(const std::string& basePath, const std::string& scaleCode);
276
277 // Load from a standalone .osd bundle file
278 static std::shared_ptr<ScaleDefinition> LoadFromOSDFile(const std::string& osdPath);
279
280 // True if this scale was loaded from a .osd bundle (vs. unpacked .json)
281 bool IsSourceOSD() const { return mSourceIsOSD; }
282
283 // Validation
284 bool Validate(std::string& errorOutput);
285
286 // Getters (non-const for editing)
287 ScaleInfo& GetScaleInfo() { return mScaleInfo; }
288 std::map<std::string, ScaleParameter>& GetParameters() { return mParameters; }
289 LikertOptions& GetLikertOptions() { return mLikertOptions; }
290 std::vector<ScaleQuestion>& GetQuestions() { return mQuestions; }
291 std::vector<ScaleDimension>& GetDimensions() { return mDimensions; }
292 std::map<std::string, DimensionScoring>& GetScoring() { return mScoring; }
293 std::map<std::string, ComputedVariable>& GetComputed() { return mComputed; }
294 ReportConfig& GetReportConfig() { return mReportConfig; }
295 DataOutput& GetDataOutput() { return mDataOutput; }
296 ScaleTranslations& GetTranslations() { return mTranslations; }
297 nlohmann::json& GetRawDefinition() { return mRawDefinition; }
298
299 // Const getters
300 const ScaleInfo& GetScaleInfo() const { return mScaleInfo; }
301 const std::map<std::string, ScaleParameter>& GetParameters() const { return mParameters; }
302 const LikertOptions& GetLikertOptions() const { return mLikertOptions; }
303 const std::vector<ScaleQuestion>& GetQuestions() const { return mQuestions; }
304 const std::vector<ScaleDimension>& GetDimensions() const { return mDimensions; }
305 const std::map<std::string, DimensionScoring>& GetScoring() const { return mScoring; }
306 const std::map<std::string, ComputedVariable>& GetComputed() const { return mComputed; }
307 const ReportConfig& GetReportConfig() const { return mReportConfig; }
308 const DataOutput& GetDataOutput() const { return mDataOutput; }
309 const ScaleTranslations& GetTranslations() const { return mTranslations; }
310 const nlohmann::json& GetRawDefinition() const { return mRawDefinition; }
311
312 // Question management
313 void AddQuestion(const ScaleQuestion& question);
314 void InsertQuestion(int index, const ScaleQuestion& question);
315 void RemoveQuestion(const std::string& questionID);
316 void MoveQuestion(int fromIndex, int toIndex);
317 ScaleQuestion* GetQuestion(const std::string& questionID);
318
319 // Dimension management
320 void AddDimension(const ScaleDimension& dimension);
321 void RemoveDimension(const std::string& dimensionID);
322 ScaleDimension* GetDimension(const std::string& dimensionID);
323
324 // Translation management
325 void AddTranslation(const std::string& language, const std::string& key, const std::string& value);
326 void RemoveTranslation(const std::string& language);
327 std::vector<std::string> GetAvailableLanguages() const;
328 std::string GetTranslation(const std::string& language, const std::string& key) const;
329
330 // Validation helpers
332 std::vector<std::string> errors;
333 std::vector<std::string> warnings;
334 bool IsValid() const { return errors.empty(); }
335 };
336 ValidationResult ValidateInternal() const;
337
338 // Default required setting (-1 = type defaults, 0 = all optional, 1 = all required)
339 int GetDefaultRequired() const { return mDefaultRequired; }
340 void SetDefaultRequired(int val) { mDefaultRequired = val; mDirty = true; }
341
342 // Dirty flag for unsaved changes
343 bool IsDirty() const { return mDirty; }
344 void SetDirty(bool dirty) { mDirty = dirty; }
345
346private:
347 bool LoadDefinitionJSON(const std::string& jsonPath);
348 bool ParseDefinitionFromJSON(const nlohmann::json& j); // Parse already-loaded JSON object
349 bool LoadFromOSDBundlePath(const std::string& osdPath); // Load from .osd bundle file
350 bool LoadTranslationJSON(const std::string& jsonPath, const std::string& language);
351 bool SaveDefinitionJSON(const std::string& jsonPath);
352 bool SaveTranslationJSON(const std::string& jsonPath, const std::string& language);
353 bool BuildDefinitionJSONObject(nlohmann::json& outJSON) const; // Build definition JSON from memory
354
355 ScaleInfo mScaleInfo;
356 std::map<std::string, ScaleParameter> mParameters;
357 LikertOptions mLikertOptions;
358 std::vector<ScaleQuestion> mQuestions;
359 std::vector<ScaleDimension> mDimensions;
360 std::map<std::string, DimensionScoring> mScoring; // dimension_id -> scoring
361 std::map<std::string, ComputedVariable> mComputed; // computed variable name -> definition
362 ReportConfig mReportConfig;
363 DataOutput mDataOutput;
364 ScaleTranslations mTranslations;
365 int mDefaultRequired; // -1 = type defaults, 0 = all optional, 1 = all required
366 bool mDirty;
367 bool mSourceIsOSD; // true if loaded from a .osd bundle file
368
369 // Raw JSON from original file - preserves unknown fields (pages, response_footer, etc.)
370 nlohmann::json mRawDefinition;
371};
372
373#endif // SCALE_DEFINITION_H
std::map< std::string, std::map< std::string, std::string > > ScaleTranslations
bool Validate(std::string &errorOutput)
ScaleDimension * GetDimension(const std::string &dimensionID)
const ScaleInfo & GetScaleInfo() const
const ScaleTranslations & GetTranslations() const
std::map< std::string, ScaleParameter > & GetParameters()
void InsertQuestion(int index, const ScaleQuestion &question)
std::string GetTranslation(const std::string &language, const std::string &key) const
const std::vector< ScaleQuestion > & GetQuestions() const
void RemoveTranslation(const std::string &language)
bool LoadFromScalesDir(const std::string &basePath, const std::string &scaleCode)
void RemoveDimension(const std::string &dimensionID)
const nlohmann::json & GetRawDefinition() const
void AddDimension(const ScaleDimension &dimension)
std::vector< std::string > GetAvailableLanguages() const
bool IsDirty() const
static std::shared_ptr< ScaleDefinition > LoadFromFile(const std::string &jsonPath)
ScaleInfo & GetScaleInfo()
const ReportConfig & GetReportConfig() const
const DataOutput & GetDataOutput() const
nlohmann::json & GetRawDefinition()
std::map< std::string, ComputedVariable > & GetComputed()
std::vector< ScaleQuestion > & GetQuestions()
bool SaveToFile(const std::string &jsonPath)
bool IsSourceOSD() const
std::map< std::string, DimensionScoring > & GetScoring()
void SetDefaultRequired(int val)
ScaleQuestion * GetQuestion(const std::string &questionID)
const LikertOptions & GetLikertOptions() const
LikertOptions & GetLikertOptions()
static std::shared_ptr< ScaleDefinition > CreateNew(const std::string &code)
ValidationResult ValidateInternal() const
void AddTranslation(const std::string &language, const std::string &key, const std::string &value)
bool ExportToJSON(const std::string &definitionsPath, const std::string &translationsPath)
ScaleTranslations & GetTranslations()
void AddQuestion(const ScaleQuestion &question)
static std::shared_ptr< ScaleDefinition > LoadFromOSDFile(const std::string &osdPath)
int GetDefaultRequired() const
void MoveQuestion(int fromIndex, int toIndex)
void SetDirty(bool dirty)
const std::map< std::string, ComputedVariable > & GetComputed() const
const std::map< std::string, DimensionScoring > & GetScoring() const
const std::map< std::string, ScaleParameter > & GetParameters() const
bool ExportToOSD(const std::string &outputDir)
const std::vector< ScaleDimension > & GetDimensions() const
std::vector< ScaleDimension > & GetDimensions()
DataOutput & GetDataOutput()
ReportConfig & GetReportConfig()
void RemoveQuestion(const std::string &questionID)
std::string expression
std::vector< NormThreshold > norms
std::string report_file
std::string pooled_file
std::string columns
std::string pooled_columns
std::string individual_file
std::vector< std::string > scores
std::map< std::string, std::vector< std::string > > correct_answers
std::vector< TransformStep > transform
std::vector< std::string > items
std::map< std::string, int > item_coding
std::map< std::string, double > weights
std::vector< NormThreshold > norms
std::map< std::string, std::vector< double > > value_map
std::string description
std::vector< std::string > labels
std::string question_head
std::string label
NormThreshold(double mn, double mx, const std::string &lbl)
std::string max_words_error
std::string pattern_error
std::string number_max_error
std::string number_min_error
std::string min_words_error
std::string max_selected_error
std::string min_length_error
std::string min_selected_error
std::string max_length_error
bool HasAnyValidation() const
std::vector< std::string > include
std::vector< std::string > footer_refs
std::string template_type
std::string header
std::vector< std::string > warnings
std::vector< std::string > errors
std::string name
std::vector< VisibleWhenCondition > visible_when
std::string abbreviation
ScaleDimension(const std::string &i, const std::string &n, const std::string &abbr)
std::string enabled_param
std::string description
std::string visible_when_logic
std::string name
std::string license
std::string abbreviation
std::string code
std::string license_url
std::string description
std::string citation
std::string url
std::string license_explanation
std::string version
std::string domain
std::string description
std::string defaultValue
ScaleParameter()=default
std::string type
ScaleParameter(const std::string &t, const std::string &d, const std::string &desc)
std::vector< std::string > options
std::string vas_orientation
std::string gate_terminate_message_key
std::string right_label
std::string image
std::vector< VisibleWhenCondition > visible_when_simple
std::vector< std::string > rows
std::vector< VasAnchor > vas_anchors
QuestionValidation validation
std::vector< std::string > options
std::string type
std::string text_key
std::string answer_alias
std::string gate_required_value
std::string left_label
std::vector< std::string > correct
std::string dimension
std::vector< std::string > section_randomize_fixed
std::vector< std::string > columns
std::vector< std::string > likert_labels
std::string visible_when_logic
std::string question_head
std::string gate_operator
TransformStep(const std::string &o, double v)
std::vector< std::string > values