bead 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bead/__init__.py +11 -0
- bead/__main__.py +11 -0
- bead/active_learning/__init__.py +15 -0
- bead/active_learning/config.py +231 -0
- bead/active_learning/loop.py +566 -0
- bead/active_learning/models/__init__.py +24 -0
- bead/active_learning/models/base.py +852 -0
- bead/active_learning/models/binary.py +910 -0
- bead/active_learning/models/categorical.py +943 -0
- bead/active_learning/models/cloze.py +862 -0
- bead/active_learning/models/forced_choice.py +956 -0
- bead/active_learning/models/free_text.py +773 -0
- bead/active_learning/models/lora.py +365 -0
- bead/active_learning/models/magnitude.py +835 -0
- bead/active_learning/models/multi_select.py +795 -0
- bead/active_learning/models/ordinal_scale.py +811 -0
- bead/active_learning/models/peft_adapter.py +155 -0
- bead/active_learning/models/random_effects.py +639 -0
- bead/active_learning/selection.py +354 -0
- bead/active_learning/strategies.py +391 -0
- bead/active_learning/trainers/__init__.py +26 -0
- bead/active_learning/trainers/base.py +210 -0
- bead/active_learning/trainers/data_collator.py +172 -0
- bead/active_learning/trainers/dataset_utils.py +261 -0
- bead/active_learning/trainers/huggingface.py +304 -0
- bead/active_learning/trainers/lightning.py +324 -0
- bead/active_learning/trainers/metrics.py +424 -0
- bead/active_learning/trainers/mixed_effects.py +551 -0
- bead/active_learning/trainers/model_wrapper.py +509 -0
- bead/active_learning/trainers/registry.py +104 -0
- bead/adapters/__init__.py +11 -0
- bead/adapters/huggingface.py +61 -0
- bead/behavioral/__init__.py +116 -0
- bead/behavioral/analytics.py +646 -0
- bead/behavioral/extraction.py +343 -0
- bead/behavioral/merging.py +343 -0
- bead/cli/__init__.py +11 -0
- bead/cli/active_learning.py +513 -0
- bead/cli/active_learning_commands.py +779 -0
- bead/cli/completion.py +359 -0
- bead/cli/config.py +624 -0
- bead/cli/constraint_builders.py +286 -0
- bead/cli/deployment.py +859 -0
- bead/cli/deployment_trials.py +493 -0
- bead/cli/deployment_ui.py +332 -0
- bead/cli/display.py +378 -0
- bead/cli/items.py +960 -0
- bead/cli/items_factories.py +776 -0
- bead/cli/list_constraints.py +714 -0
- bead/cli/lists.py +490 -0
- bead/cli/main.py +430 -0
- bead/cli/models.py +877 -0
- bead/cli/resource_loaders.py +621 -0
- bead/cli/resources.py +1036 -0
- bead/cli/shell.py +356 -0
- bead/cli/simulate.py +840 -0
- bead/cli/templates.py +1158 -0
- bead/cli/training.py +1080 -0
- bead/cli/utils.py +614 -0
- bead/cli/workflow.py +1273 -0
- bead/config/__init__.py +68 -0
- bead/config/active_learning.py +1009 -0
- bead/config/config.py +192 -0
- bead/config/defaults.py +118 -0
- bead/config/deployment.py +217 -0
- bead/config/env.py +147 -0
- bead/config/item.py +45 -0
- bead/config/list.py +193 -0
- bead/config/loader.py +149 -0
- bead/config/logging.py +42 -0
- bead/config/model.py +49 -0
- bead/config/paths.py +46 -0
- bead/config/profiles.py +320 -0
- bead/config/resources.py +47 -0
- bead/config/serialization.py +210 -0
- bead/config/simulation.py +206 -0
- bead/config/template.py +238 -0
- bead/config/validation.py +267 -0
- bead/data/__init__.py +65 -0
- bead/data/base.py +87 -0
- bead/data/identifiers.py +97 -0
- bead/data/language_codes.py +61 -0
- bead/data/metadata.py +270 -0
- bead/data/range.py +123 -0
- bead/data/repository.py +358 -0
- bead/data/serialization.py +249 -0
- bead/data/timestamps.py +89 -0
- bead/data/validation.py +349 -0
- bead/data_collection/__init__.py +11 -0
- bead/data_collection/jatos.py +223 -0
- bead/data_collection/merger.py +154 -0
- bead/data_collection/prolific.py +198 -0
- bead/deployment/__init__.py +5 -0
- bead/deployment/distribution.py +402 -0
- bead/deployment/jatos/__init__.py +1 -0
- bead/deployment/jatos/api.py +200 -0
- bead/deployment/jatos/exporter.py +210 -0
- bead/deployment/jspsych/__init__.py +9 -0
- bead/deployment/jspsych/biome.json +44 -0
- bead/deployment/jspsych/config.py +411 -0
- bead/deployment/jspsych/generator.py +598 -0
- bead/deployment/jspsych/package.json +51 -0
- bead/deployment/jspsych/pnpm-lock.yaml +2141 -0
- bead/deployment/jspsych/randomizer.py +299 -0
- bead/deployment/jspsych/src/lib/list-distributor.test.ts +327 -0
- bead/deployment/jspsych/src/lib/list-distributor.ts +1282 -0
- bead/deployment/jspsych/src/lib/randomizer.test.ts +232 -0
- bead/deployment/jspsych/src/lib/randomizer.ts +367 -0
- bead/deployment/jspsych/src/plugins/cloze-dropdown.ts +252 -0
- bead/deployment/jspsych/src/plugins/forced-choice.ts +265 -0
- bead/deployment/jspsych/src/plugins/plugins.test.ts +141 -0
- bead/deployment/jspsych/src/plugins/rating.ts +248 -0
- bead/deployment/jspsych/src/slopit/index.ts +9 -0
- bead/deployment/jspsych/src/types/jatos.d.ts +256 -0
- bead/deployment/jspsych/src/types/jspsych.d.ts +228 -0
- bead/deployment/jspsych/templates/experiment.css +1 -0
- bead/deployment/jspsych/templates/experiment.js.template +289 -0
- bead/deployment/jspsych/templates/index.html +51 -0
- bead/deployment/jspsych/templates/randomizer.js +241 -0
- bead/deployment/jspsych/templates/randomizer.js.template +313 -0
- bead/deployment/jspsych/trials.py +723 -0
- bead/deployment/jspsych/tsconfig.json +23 -0
- bead/deployment/jspsych/tsup.config.ts +30 -0
- bead/deployment/jspsych/ui/__init__.py +1 -0
- bead/deployment/jspsych/ui/components.py +383 -0
- bead/deployment/jspsych/ui/styles.py +411 -0
- bead/dsl/__init__.py +80 -0
- bead/dsl/ast.py +168 -0
- bead/dsl/context.py +178 -0
- bead/dsl/errors.py +71 -0
- bead/dsl/evaluator.py +570 -0
- bead/dsl/grammar.lark +81 -0
- bead/dsl/parser.py +231 -0
- bead/dsl/stdlib.py +929 -0
- bead/evaluation/__init__.py +13 -0
- bead/evaluation/convergence.py +485 -0
- bead/evaluation/interannotator.py +398 -0
- bead/items/__init__.py +40 -0
- bead/items/adapters/__init__.py +70 -0
- bead/items/adapters/anthropic.py +224 -0
- bead/items/adapters/api_utils.py +167 -0
- bead/items/adapters/base.py +216 -0
- bead/items/adapters/google.py +259 -0
- bead/items/adapters/huggingface.py +1074 -0
- bead/items/adapters/openai.py +323 -0
- bead/items/adapters/registry.py +202 -0
- bead/items/adapters/sentence_transformers.py +224 -0
- bead/items/adapters/togetherai.py +309 -0
- bead/items/binary.py +515 -0
- bead/items/cache.py +558 -0
- bead/items/categorical.py +593 -0
- bead/items/cloze.py +757 -0
- bead/items/constructor.py +784 -0
- bead/items/forced_choice.py +413 -0
- bead/items/free_text.py +681 -0
- bead/items/generation.py +432 -0
- bead/items/item.py +396 -0
- bead/items/item_template.py +787 -0
- bead/items/magnitude.py +573 -0
- bead/items/multi_select.py +621 -0
- bead/items/ordinal_scale.py +569 -0
- bead/items/scoring.py +448 -0
- bead/items/validation.py +723 -0
- bead/lists/__init__.py +30 -0
- bead/lists/balancer.py +263 -0
- bead/lists/constraints.py +1067 -0
- bead/lists/experiment_list.py +286 -0
- bead/lists/list_collection.py +378 -0
- bead/lists/partitioner.py +1141 -0
- bead/lists/stratification.py +254 -0
- bead/participants/__init__.py +73 -0
- bead/participants/collection.py +699 -0
- bead/participants/merging.py +312 -0
- bead/participants/metadata_spec.py +491 -0
- bead/participants/models.py +276 -0
- bead/resources/__init__.py +29 -0
- bead/resources/adapters/__init__.py +19 -0
- bead/resources/adapters/base.py +104 -0
- bead/resources/adapters/cache.py +128 -0
- bead/resources/adapters/glazing.py +508 -0
- bead/resources/adapters/registry.py +117 -0
- bead/resources/adapters/unimorph.py +796 -0
- bead/resources/classification.py +856 -0
- bead/resources/constraint_builders.py +329 -0
- bead/resources/constraints.py +165 -0
- bead/resources/lexical_item.py +223 -0
- bead/resources/lexicon.py +744 -0
- bead/resources/loaders.py +209 -0
- bead/resources/template.py +441 -0
- bead/resources/template_collection.py +707 -0
- bead/resources/template_generation.py +349 -0
- bead/simulation/__init__.py +29 -0
- bead/simulation/annotators/__init__.py +15 -0
- bead/simulation/annotators/base.py +175 -0
- bead/simulation/annotators/distance_based.py +135 -0
- bead/simulation/annotators/lm_based.py +114 -0
- bead/simulation/annotators/oracle.py +182 -0
- bead/simulation/annotators/random.py +181 -0
- bead/simulation/dsl_extension/__init__.py +3 -0
- bead/simulation/noise_models/__init__.py +13 -0
- bead/simulation/noise_models/base.py +42 -0
- bead/simulation/noise_models/random_noise.py +82 -0
- bead/simulation/noise_models/systematic.py +132 -0
- bead/simulation/noise_models/temperature.py +86 -0
- bead/simulation/runner.py +144 -0
- bead/simulation/strategies/__init__.py +23 -0
- bead/simulation/strategies/base.py +123 -0
- bead/simulation/strategies/binary.py +103 -0
- bead/simulation/strategies/categorical.py +123 -0
- bead/simulation/strategies/cloze.py +224 -0
- bead/simulation/strategies/forced_choice.py +127 -0
- bead/simulation/strategies/free_text.py +105 -0
- bead/simulation/strategies/magnitude.py +116 -0
- bead/simulation/strategies/multi_select.py +129 -0
- bead/simulation/strategies/ordinal_scale.py +131 -0
- bead/templates/__init__.py +27 -0
- bead/templates/adapters/__init__.py +17 -0
- bead/templates/adapters/base.py +128 -0
- bead/templates/adapters/cache.py +178 -0
- bead/templates/adapters/huggingface.py +312 -0
- bead/templates/combinatorics.py +103 -0
- bead/templates/filler.py +605 -0
- bead/templates/renderers.py +177 -0
- bead/templates/resolver.py +178 -0
- bead/templates/strategies.py +1806 -0
- bead/templates/streaming.py +195 -0
- bead-0.1.0.dist-info/METADATA +212 -0
- bead-0.1.0.dist-info/RECORD +231 -0
- bead-0.1.0.dist-info/WHEEL +4 -0
- bead-0.1.0.dist-info/entry_points.txt +2 -0
- bead-0.1.0.dist-info/licenses/LICENSE +21 -0
bead/cli/deployment.py
ADDED
|
@@ -0,0 +1,859 @@
|
|
|
1
|
+
"""Deployment commands for bead CLI.
|
|
2
|
+
|
|
3
|
+
This module provides commands for generating and deploying jsPsych experiments
|
|
4
|
+
(Stage 5 of the bead pipeline).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import cast
|
|
12
|
+
from uuid import UUID
|
|
13
|
+
|
|
14
|
+
import click
|
|
15
|
+
from pydantic import ValidationError
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
18
|
+
|
|
19
|
+
from bead.cli.utils import print_error, print_info, print_success
|
|
20
|
+
from bead.data.base import JsonValue
|
|
21
|
+
from bead.deployment.distribution import (
|
|
22
|
+
DistributionStrategyType,
|
|
23
|
+
ListDistributionStrategy,
|
|
24
|
+
)
|
|
25
|
+
from bead.deployment.jatos.api import JATOSClient
|
|
26
|
+
from bead.deployment.jatos.exporter import JATOSExporter
|
|
27
|
+
from bead.deployment.jspsych.config import ExperimentConfig
|
|
28
|
+
from bead.deployment.jspsych.generator import JsPsychExperimentGenerator
|
|
29
|
+
from bead.items.item import Item
|
|
30
|
+
from bead.items.item_template import ItemTemplate, PresentationSpec, TaskSpec
|
|
31
|
+
from bead.lists import ExperimentList
|
|
32
|
+
|
|
33
|
+
console = Console()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@click.group()
|
|
37
|
+
def deployment() -> None:
|
|
38
|
+
r"""Deployment commands (Stage 5).
|
|
39
|
+
|
|
40
|
+
Commands for generating and deploying jsPsych experiments.
|
|
41
|
+
|
|
42
|
+
\b
|
|
43
|
+
Examples:
|
|
44
|
+
$ bead deployment generate lists.jsonl items.jsonl experiment/
|
|
45
|
+
$ bead deployment export-jatos experiment/ study.jzip \\
|
|
46
|
+
--title "My Study"
|
|
47
|
+
$ bead deployment upload-jatos study.jzip \\
|
|
48
|
+
--jatos-url https://jatos.example.com --api-token TOKEN
|
|
49
|
+
$ bead deployment validate experiment/
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@click.command()
|
|
54
|
+
@click.argument(
|
|
55
|
+
"lists_file", type=click.Path(exists=True, dir_okay=False, path_type=Path)
|
|
56
|
+
)
|
|
57
|
+
@click.argument(
|
|
58
|
+
"items_file", type=click.Path(exists=True, dir_okay=False, path_type=Path)
|
|
59
|
+
)
|
|
60
|
+
@click.argument("output_dir", type=click.Path(path_type=Path))
|
|
61
|
+
@click.option(
|
|
62
|
+
"--experiment-type",
|
|
63
|
+
type=click.Choice(["likert_rating", "forced_choice", "magnitude_estimation"]),
|
|
64
|
+
default="likert_rating",
|
|
65
|
+
help="Type of experiment",
|
|
66
|
+
)
|
|
67
|
+
@click.option("--title", default="Experiment", help="Experiment title")
|
|
68
|
+
@click.option("--description", default="", help="Experiment description")
|
|
69
|
+
@click.option(
|
|
70
|
+
"--instructions", default="Please complete the task.", help="Instructions text"
|
|
71
|
+
)
|
|
72
|
+
@click.option(
|
|
73
|
+
"--distribution-strategy",
|
|
74
|
+
type=click.Choice(
|
|
75
|
+
[
|
|
76
|
+
"random",
|
|
77
|
+
"sequential",
|
|
78
|
+
"balanced",
|
|
79
|
+
"latin_square",
|
|
80
|
+
"stratified",
|
|
81
|
+
"weighted_random",
|
|
82
|
+
"quota_based",
|
|
83
|
+
"metadata_based",
|
|
84
|
+
],
|
|
85
|
+
case_sensitive=False,
|
|
86
|
+
),
|
|
87
|
+
required=True,
|
|
88
|
+
help="List distribution strategy (REQUIRED, no default). "
|
|
89
|
+
"random: Random selection. "
|
|
90
|
+
"sequential: Round-robin. "
|
|
91
|
+
"balanced: Assign to least-used list. "
|
|
92
|
+
"latin_square: Counterbalancing. "
|
|
93
|
+
"stratified: Balance across factors. "
|
|
94
|
+
"weighted_random: Non-uniform probabilities. "
|
|
95
|
+
"quota_based: Fixed quota per list. "
|
|
96
|
+
"metadata_based: Filter/rank by metadata.",
|
|
97
|
+
)
|
|
98
|
+
@click.option(
|
|
99
|
+
"--distribution-config",
|
|
100
|
+
type=str,
|
|
101
|
+
help="Strategy-specific configuration (JSON format). "
|
|
102
|
+
"Examples: "
|
|
103
|
+
'quota_based: \'{"participants_per_list": 25, "allow_overflow": false}\'. '
|
|
104
|
+
'weighted_random: \'{"weight_expression": "list_metadata.priority || 1.0"}\'. '
|
|
105
|
+
'stratified: \'{"factors": ["condition", "verb_type"]}\'. '
|
|
106
|
+
"metadata_based: "
|
|
107
|
+
"'{\"filter_expression\": \"list_metadata.difficulty === 'easy'\"}'. ",
|
|
108
|
+
)
|
|
109
|
+
@click.option(
|
|
110
|
+
"--max-participants",
|
|
111
|
+
type=int,
|
|
112
|
+
help="Maximum total participants across all lists (unlimited if not specified)",
|
|
113
|
+
)
|
|
114
|
+
@click.option(
|
|
115
|
+
"--debug-mode",
|
|
116
|
+
is_flag=True,
|
|
117
|
+
help="Enable debug mode (always assign same list for testing)",
|
|
118
|
+
)
|
|
119
|
+
@click.option(
|
|
120
|
+
"--debug-list-index",
|
|
121
|
+
type=int,
|
|
122
|
+
default=0,
|
|
123
|
+
help="List index to use in debug mode (default: 0)",
|
|
124
|
+
)
|
|
125
|
+
@click.option(
|
|
126
|
+
"--dry-run",
|
|
127
|
+
is_flag=True,
|
|
128
|
+
help="Show what would be done without generating files",
|
|
129
|
+
)
|
|
130
|
+
@click.pass_context
|
|
131
|
+
def generate(
|
|
132
|
+
ctx: click.Context,
|
|
133
|
+
lists_file: Path,
|
|
134
|
+
items_file: Path,
|
|
135
|
+
output_dir: Path,
|
|
136
|
+
experiment_type: str,
|
|
137
|
+
title: str,
|
|
138
|
+
description: str,
|
|
139
|
+
instructions: str,
|
|
140
|
+
distribution_strategy: str,
|
|
141
|
+
distribution_config: str | None,
|
|
142
|
+
max_participants: int | None,
|
|
143
|
+
debug_mode: bool,
|
|
144
|
+
debug_list_index: int,
|
|
145
|
+
dry_run: bool,
|
|
146
|
+
) -> None:
|
|
147
|
+
r"""Generate jsPsych experiment from lists and items.
|
|
148
|
+
|
|
149
|
+
Parameters
|
|
150
|
+
----------
|
|
151
|
+
ctx : click.Context
|
|
152
|
+
Click context object.
|
|
153
|
+
lists_file : Path
|
|
154
|
+
JSONL file containing experiment lists (one list per line).
|
|
155
|
+
items_file : Path
|
|
156
|
+
JSONL file containing items (one item per line).
|
|
157
|
+
output_dir : Path
|
|
158
|
+
Output directory for generated experiment.
|
|
159
|
+
experiment_type : str
|
|
160
|
+
Type of experiment to generate.
|
|
161
|
+
title : str
|
|
162
|
+
Experiment title.
|
|
163
|
+
description : str
|
|
164
|
+
Experiment description.
|
|
165
|
+
instructions : str
|
|
166
|
+
Instructions text.
|
|
167
|
+
distribution_strategy : str
|
|
168
|
+
Distribution strategy type (required).
|
|
169
|
+
distribution_config : str | None
|
|
170
|
+
Strategy-specific configuration as JSON string.
|
|
171
|
+
max_participants : int | None
|
|
172
|
+
Maximum total participants.
|
|
173
|
+
debug_mode : bool
|
|
174
|
+
Enable debug mode.
|
|
175
|
+
debug_list_index : int
|
|
176
|
+
List index for debug mode.
|
|
177
|
+
dry_run : bool
|
|
178
|
+
Show what would be done without generating files.
|
|
179
|
+
|
|
180
|
+
Examples
|
|
181
|
+
--------
|
|
182
|
+
# Basic balanced distribution
|
|
183
|
+
$ bead deployment generate lists.jsonl items.jsonl experiment/ \\
|
|
184
|
+
--experiment-type forced_choice \\
|
|
185
|
+
--title "Acceptability Study" \\
|
|
186
|
+
--distribution-strategy balanced
|
|
187
|
+
|
|
188
|
+
# Quota-based with config
|
|
189
|
+
$ bead deployment generate lists.jsonl items.jsonl experiment/ \\
|
|
190
|
+
--experiment-type forced_choice \\
|
|
191
|
+
--distribution-strategy quota_based \\
|
|
192
|
+
--distribution-config '{"participants_per_list": 25, "allow_overflow": false}'
|
|
193
|
+
|
|
194
|
+
# Stratified by factors
|
|
195
|
+
$ bead deployment generate lists.jsonl items.jsonl experiment/ \\
|
|
196
|
+
--experiment-type forced_choice \\
|
|
197
|
+
--distribution-strategy stratified \\
|
|
198
|
+
--distribution-config '{"factors": ["condition", "verb_type"]}'
|
|
199
|
+
|
|
200
|
+
# Dry run to preview
|
|
201
|
+
$ bead deployment generate lists.jsonl items.jsonl experiment/ \\
|
|
202
|
+
--experiment-type forced_choice \\
|
|
203
|
+
--distribution-strategy balanced \\
|
|
204
|
+
--dry-run
|
|
205
|
+
"""
|
|
206
|
+
try:
|
|
207
|
+
# Parse distribution config if provided
|
|
208
|
+
strategy_config_dict: dict[str, JsonValue] = {}
|
|
209
|
+
if distribution_config:
|
|
210
|
+
try:
|
|
211
|
+
strategy_config_dict = json.loads(distribution_config)
|
|
212
|
+
except json.JSONDecodeError as e:
|
|
213
|
+
print_error(
|
|
214
|
+
f"Invalid JSON in --distribution-config: {e}\n"
|
|
215
|
+
f"Provided: {distribution_config}\n"
|
|
216
|
+
f"Example: '{{\"participants_per_list\": 25}}'"
|
|
217
|
+
)
|
|
218
|
+
ctx.exit(1)
|
|
219
|
+
|
|
220
|
+
# Create distribution strategy
|
|
221
|
+
try:
|
|
222
|
+
dist_strategy = ListDistributionStrategy(
|
|
223
|
+
strategy_type=DistributionStrategyType(distribution_strategy),
|
|
224
|
+
strategy_config=strategy_config_dict,
|
|
225
|
+
max_participants=max_participants,
|
|
226
|
+
debug_mode=debug_mode,
|
|
227
|
+
debug_list_index=debug_list_index,
|
|
228
|
+
)
|
|
229
|
+
except ValueError as e:
|
|
230
|
+
print_error(f"Invalid distribution strategy configuration: {e}")
|
|
231
|
+
ctx.exit(1)
|
|
232
|
+
# Load experiment lists from JSONL file (one list per line)
|
|
233
|
+
print_info(f"Loading experiment lists from {lists_file}")
|
|
234
|
+
experiment_lists: list[ExperimentList] = []
|
|
235
|
+
with open(lists_file, encoding="utf-8") as f:
|
|
236
|
+
for line in f:
|
|
237
|
+
line = line.strip()
|
|
238
|
+
if not line:
|
|
239
|
+
continue
|
|
240
|
+
list_data = json.loads(line)
|
|
241
|
+
exp_list = ExperimentList(**list_data)
|
|
242
|
+
experiment_lists.append(exp_list)
|
|
243
|
+
|
|
244
|
+
if not experiment_lists:
|
|
245
|
+
print_error(f"No lists found in {lists_file}")
|
|
246
|
+
ctx.exit(1)
|
|
247
|
+
|
|
248
|
+
print_info(f"Loaded {len(experiment_lists)} experiment lists")
|
|
249
|
+
|
|
250
|
+
# Load items
|
|
251
|
+
print_info(f"Loading items from {items_file}")
|
|
252
|
+
items_dict: dict[UUID, Item] = {}
|
|
253
|
+
with open(items_file, encoding="utf-8") as f:
|
|
254
|
+
for line in f:
|
|
255
|
+
line = line.strip()
|
|
256
|
+
if not line:
|
|
257
|
+
continue
|
|
258
|
+
item_data = json.loads(line)
|
|
259
|
+
item = Item(**item_data)
|
|
260
|
+
items_dict[item.id] = item
|
|
261
|
+
|
|
262
|
+
print_info(f"Loaded {len(items_dict)} items")
|
|
263
|
+
|
|
264
|
+
# Create stub templates for each unique item_template_id (simplified for CLI)
|
|
265
|
+
# Extract unique template IDs from items
|
|
266
|
+
unique_template_ids = {item.item_template_id for item in items_dict.values()}
|
|
267
|
+
templates_dict: dict[UUID, ItemTemplate] = {}
|
|
268
|
+
for template_id in unique_template_ids:
|
|
269
|
+
# Create minimal stub template (no actual template
|
|
270
|
+
# structure needed for deployment)
|
|
271
|
+
templates_dict[template_id] = ItemTemplate(
|
|
272
|
+
id=template_id,
|
|
273
|
+
name=f"template_{template_id}",
|
|
274
|
+
description="Auto-generated stub template for CLI deployment",
|
|
275
|
+
judgment_type="acceptability",
|
|
276
|
+
task_type="ordinal_scale",
|
|
277
|
+
task_spec=TaskSpec(
|
|
278
|
+
prompt="Rate this item.",
|
|
279
|
+
scale_bounds=(1, 7),
|
|
280
|
+
),
|
|
281
|
+
presentation_spec=PresentationSpec(mode="static"),
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
print_info(f"Created {len(templates_dict)} stub templates for deployment")
|
|
285
|
+
|
|
286
|
+
# Create experiment config with distribution strategy
|
|
287
|
+
config = ExperimentConfig(
|
|
288
|
+
experiment_type=experiment_type, # type: ignore
|
|
289
|
+
title=title,
|
|
290
|
+
description=description,
|
|
291
|
+
instructions=instructions,
|
|
292
|
+
distribution_strategy=dist_strategy,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
# Generate experiment (or show dry-run preview)
|
|
296
|
+
if dry_run:
|
|
297
|
+
print_info("[DRY RUN] Would generate jsPsych experiment with:")
|
|
298
|
+
console.print(f" [dim]Output directory:[/dim] {output_dir}")
|
|
299
|
+
console.print(f" [dim]Experiment type:[/dim] {experiment_type}")
|
|
300
|
+
console.print(f" [dim]Title:[/dim] {title}")
|
|
301
|
+
console.print(
|
|
302
|
+
f" [dim]Distribution strategy:[/dim] {distribution_strategy}"
|
|
303
|
+
)
|
|
304
|
+
console.print(f" [dim]Number of lists:[/dim] {len(experiment_lists)}")
|
|
305
|
+
console.print(f" [dim]Number of items:[/dim] {len(items_dict)}")
|
|
306
|
+
console.print(f" [dim]Number of templates:[/dim] {len(templates_dict)}")
|
|
307
|
+
if max_participants:
|
|
308
|
+
console.print(f" [dim]Max participants:[/dim] {max_participants}")
|
|
309
|
+
if debug_mode:
|
|
310
|
+
console.print(
|
|
311
|
+
f" [dim]Debug mode:[/dim] Enabled (list index: {debug_list_index})"
|
|
312
|
+
)
|
|
313
|
+
print_info("[DRY RUN] Files that would be created:")
|
|
314
|
+
console.print(f" [dim]{output_dir}/index.html[/dim]")
|
|
315
|
+
console.print(f" [dim]{output_dir}/js/experiment.js[/dim]")
|
|
316
|
+
console.print(f" [dim]{output_dir}/js/list_distributor.js[/dim]")
|
|
317
|
+
console.print(f" [dim]{output_dir}/css/experiment.css[/dim]")
|
|
318
|
+
console.print(f" [dim]{output_dir}/data/config.json[/dim]")
|
|
319
|
+
console.print(f" [dim]{output_dir}/data/lists.jsonl[/dim]")
|
|
320
|
+
console.print(f" [dim]{output_dir}/data/items.jsonl[/dim]")
|
|
321
|
+
console.print(f" [dim]{output_dir}/data/distribution.json[/dim]")
|
|
322
|
+
else:
|
|
323
|
+
with Progress(
|
|
324
|
+
SpinnerColumn(),
|
|
325
|
+
TextColumn("[progress.description]{task.description}"),
|
|
326
|
+
console=console,
|
|
327
|
+
) as progress:
|
|
328
|
+
progress.add_task("Generating jsPsych experiment...", total=None)
|
|
329
|
+
|
|
330
|
+
generator = JsPsychExperimentGenerator(
|
|
331
|
+
config=config,
|
|
332
|
+
output_dir=output_dir,
|
|
333
|
+
)
|
|
334
|
+
output_path = generator.generate(
|
|
335
|
+
lists=experiment_lists,
|
|
336
|
+
items=items_dict,
|
|
337
|
+
templates=templates_dict,
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
print_success(f"Generated jsPsych experiment: {output_path}")
|
|
341
|
+
|
|
342
|
+
except ValidationError as e:
|
|
343
|
+
print_error(f"Validation error: {e}")
|
|
344
|
+
ctx.exit(1)
|
|
345
|
+
except Exception as e:
|
|
346
|
+
print_error(f"Failed to generate experiment: {e}")
|
|
347
|
+
ctx.exit(1)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
@click.command()
|
|
351
|
+
@click.argument(
|
|
352
|
+
"experiment_dir", type=click.Path(exists=True, file_okay=False, path_type=Path)
|
|
353
|
+
)
|
|
354
|
+
@click.argument("output_file", type=click.Path(path_type=Path))
|
|
355
|
+
@click.option("--title", required=True, help="Study title for JATOS")
|
|
356
|
+
@click.option("--description", default="", help="Study description")
|
|
357
|
+
@click.option("--component-title", default="Main Experiment", help="Component title")
|
|
358
|
+
@click.pass_context
|
|
359
|
+
def export_jatos(
|
|
360
|
+
ctx: click.Context,
|
|
361
|
+
experiment_dir: Path,
|
|
362
|
+
output_file: Path,
|
|
363
|
+
title: str,
|
|
364
|
+
description: str,
|
|
365
|
+
component_title: str,
|
|
366
|
+
) -> None:
|
|
367
|
+
r"""Export experiment to JATOS .jzip file.
|
|
368
|
+
|
|
369
|
+
Parameters
|
|
370
|
+
----------
|
|
371
|
+
ctx : click.Context
|
|
372
|
+
Click context object.
|
|
373
|
+
experiment_dir : Path
|
|
374
|
+
Directory containing generated experiment.
|
|
375
|
+
output_file : Path
|
|
376
|
+
Output path for .jzip file.
|
|
377
|
+
title : str
|
|
378
|
+
Study title for JATOS.
|
|
379
|
+
description : str
|
|
380
|
+
Study description.
|
|
381
|
+
component_title : str
|
|
382
|
+
Component title.
|
|
383
|
+
|
|
384
|
+
Examples
|
|
385
|
+
--------
|
|
386
|
+
$ bead deployment export-jatos experiment/ study.jzip \\
|
|
387
|
+
--title "Acceptability Study" \\
|
|
388
|
+
--description "Rating task for linguistic acceptability"
|
|
389
|
+
"""
|
|
390
|
+
try:
|
|
391
|
+
print_info(f"Exporting experiment from {experiment_dir}")
|
|
392
|
+
|
|
393
|
+
with Progress(
|
|
394
|
+
SpinnerColumn(),
|
|
395
|
+
TextColumn("[progress.description]{task.description}"),
|
|
396
|
+
console=console,
|
|
397
|
+
) as progress:
|
|
398
|
+
progress.add_task("Creating JATOS package...", total=None)
|
|
399
|
+
|
|
400
|
+
exporter = JATOSExporter(
|
|
401
|
+
study_title=title,
|
|
402
|
+
study_description=description,
|
|
403
|
+
)
|
|
404
|
+
exporter.export(
|
|
405
|
+
experiment_dir=experiment_dir,
|
|
406
|
+
output_path=output_file,
|
|
407
|
+
component_title=component_title,
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
print_success(f"Created JATOS package: {output_file}")
|
|
411
|
+
|
|
412
|
+
except FileNotFoundError as e:
|
|
413
|
+
print_error(f"File not found: {e}")
|
|
414
|
+
ctx.exit(1)
|
|
415
|
+
except ValueError as e:
|
|
416
|
+
print_error(f"Invalid experiment: {e}")
|
|
417
|
+
ctx.exit(1)
|
|
418
|
+
except Exception as e:
|
|
419
|
+
print_error(f"Failed to export to JATOS: {e}")
|
|
420
|
+
ctx.exit(1)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
@click.command()
|
|
424
|
+
@click.argument("jzip_file", type=click.Path(exists=True, path_type=Path))
|
|
425
|
+
@click.option("--jatos-url", required=True, help="JATOS server URL")
|
|
426
|
+
@click.option("--api-token", required=True, help="JATOS API token")
|
|
427
|
+
@click.pass_context
|
|
428
|
+
def upload_jatos(
|
|
429
|
+
ctx: click.Context,
|
|
430
|
+
jzip_file: Path,
|
|
431
|
+
jatos_url: str,
|
|
432
|
+
api_token: str,
|
|
433
|
+
) -> None:
|
|
434
|
+
r"""Upload .jzip file to JATOS server.
|
|
435
|
+
|
|
436
|
+
Parameters
|
|
437
|
+
----------
|
|
438
|
+
ctx : click.Context
|
|
439
|
+
Click context object.
|
|
440
|
+
jzip_file : Path
|
|
441
|
+
Path to .jzip file.
|
|
442
|
+
jatos_url : str
|
|
443
|
+
JATOS server URL.
|
|
444
|
+
api_token : str
|
|
445
|
+
JATOS API token.
|
|
446
|
+
|
|
447
|
+
Examples
|
|
448
|
+
--------
|
|
449
|
+
$ bead deployment upload-jatos study.jzip \\
|
|
450
|
+
--jatos-url https://jatos.example.com \\
|
|
451
|
+
--api-token my-api-token
|
|
452
|
+
"""
|
|
453
|
+
try:
|
|
454
|
+
print_info(f"Uploading {jzip_file} to {jatos_url}")
|
|
455
|
+
|
|
456
|
+
with Progress(
|
|
457
|
+
SpinnerColumn(),
|
|
458
|
+
TextColumn("[progress.description]{task.description}"),
|
|
459
|
+
console=console,
|
|
460
|
+
) as progress:
|
|
461
|
+
progress.add_task("Uploading to JATOS...", total=None)
|
|
462
|
+
|
|
463
|
+
client = JATOSClient(base_url=jatos_url, api_token=api_token)
|
|
464
|
+
study_id: int = client.import_study(jzip_file) # type: ignore[attr-defined]
|
|
465
|
+
|
|
466
|
+
print_success(f"Uploaded study to JATOS (Study ID: {study_id})")
|
|
467
|
+
|
|
468
|
+
except Exception as e:
|
|
469
|
+
print_error(f"Failed to upload to JATOS: {e}")
|
|
470
|
+
ctx.exit(1)
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
@click.command()
|
|
474
|
+
@click.argument(
|
|
475
|
+
"experiment_dir", type=click.Path(exists=True, file_okay=False, path_type=Path)
|
|
476
|
+
)
|
|
477
|
+
@click.option(
|
|
478
|
+
"--check-distribution",
|
|
479
|
+
is_flag=True,
|
|
480
|
+
default=False,
|
|
481
|
+
help="Validate distribution strategy configuration",
|
|
482
|
+
)
|
|
483
|
+
@click.option(
|
|
484
|
+
"--check-trials",
|
|
485
|
+
is_flag=True,
|
|
486
|
+
default=False,
|
|
487
|
+
help="Validate trial configurations (if present)",
|
|
488
|
+
)
|
|
489
|
+
@click.option(
|
|
490
|
+
"--check-data-structure",
|
|
491
|
+
is_flag=True,
|
|
492
|
+
default=False,
|
|
493
|
+
help="Validate JSONL data structure and schemas",
|
|
494
|
+
)
|
|
495
|
+
@click.option(
|
|
496
|
+
"--strict",
|
|
497
|
+
is_flag=True,
|
|
498
|
+
default=False,
|
|
499
|
+
help="Enable all validation checks (strict mode)",
|
|
500
|
+
)
|
|
501
|
+
@click.pass_context
|
|
502
|
+
def validate(
|
|
503
|
+
ctx: click.Context,
|
|
504
|
+
experiment_dir: Path,
|
|
505
|
+
check_distribution: bool,
|
|
506
|
+
check_trials: bool,
|
|
507
|
+
check_data_structure: bool,
|
|
508
|
+
strict: bool,
|
|
509
|
+
) -> None:
|
|
510
|
+
"""Validate generated experiment structure.
|
|
511
|
+
|
|
512
|
+
Parameters
|
|
513
|
+
----------
|
|
514
|
+
ctx : click.Context
|
|
515
|
+
Click context object.
|
|
516
|
+
experiment_dir : Path
|
|
517
|
+
Directory containing generated experiment.
|
|
518
|
+
check_distribution : bool
|
|
519
|
+
Validate distribution strategy configuration.
|
|
520
|
+
check_trials : bool
|
|
521
|
+
Validate trial configurations (if present).
|
|
522
|
+
check_data_structure : bool
|
|
523
|
+
Validate JSONL data structure and schemas.
|
|
524
|
+
strict : bool
|
|
525
|
+
Enable all validation checks.
|
|
526
|
+
|
|
527
|
+
Examples
|
|
528
|
+
--------
|
|
529
|
+
$ bead deployment validate experiment/
|
|
530
|
+
|
|
531
|
+
$ bead deployment validate experiment/ --check-distribution
|
|
532
|
+
|
|
533
|
+
$ bead deployment validate experiment/ --strict
|
|
534
|
+
"""
|
|
535
|
+
try:
|
|
536
|
+
# Enable all checks if strict mode is enabled
|
|
537
|
+
if strict:
|
|
538
|
+
check_distribution = True
|
|
539
|
+
check_trials = True
|
|
540
|
+
check_data_structure = True
|
|
541
|
+
|
|
542
|
+
print_info(f"Validating experiment: {experiment_dir}")
|
|
543
|
+
if strict:
|
|
544
|
+
print_info("Running in strict mode (all checks enabled)")
|
|
545
|
+
|
|
546
|
+
validation_errors: list[str] = []
|
|
547
|
+
validation_warnings: list[str] = []
|
|
548
|
+
|
|
549
|
+
# Check required files (batch mode)
|
|
550
|
+
required_files = [
|
|
551
|
+
"index.html",
|
|
552
|
+
"css/experiment.css",
|
|
553
|
+
"js/experiment.js",
|
|
554
|
+
"js/list_distributor.js",
|
|
555
|
+
"data/config.json",
|
|
556
|
+
"data/lists.jsonl",
|
|
557
|
+
"data/items.jsonl",
|
|
558
|
+
"data/distribution.json",
|
|
559
|
+
]
|
|
560
|
+
|
|
561
|
+
missing_files: list[str] = []
|
|
562
|
+
for file_path in required_files:
|
|
563
|
+
full_path = experiment_dir / file_path
|
|
564
|
+
if not full_path.exists():
|
|
565
|
+
missing_files.append(file_path)
|
|
566
|
+
|
|
567
|
+
if missing_files:
|
|
568
|
+
for file_path in missing_files:
|
|
569
|
+
validation_errors.append(f"Missing required file: {file_path}")
|
|
570
|
+
|
|
571
|
+
# Validate lists.jsonl
|
|
572
|
+
lists_file = experiment_dir / "data" / "lists.jsonl"
|
|
573
|
+
if lists_file.exists():
|
|
574
|
+
with open(lists_file, encoding="utf-8") as f:
|
|
575
|
+
lists_data = [json.loads(line) for line in f if line.strip()]
|
|
576
|
+
|
|
577
|
+
if not lists_data:
|
|
578
|
+
validation_errors.append("lists.jsonl must contain at least one list")
|
|
579
|
+
else:
|
|
580
|
+
lists_data = []
|
|
581
|
+
|
|
582
|
+
# Validate items.jsonl
|
|
583
|
+
items_file = experiment_dir / "data" / "items.jsonl"
|
|
584
|
+
if items_file.exists():
|
|
585
|
+
with open(items_file, encoding="utf-8") as f:
|
|
586
|
+
items_data = [json.loads(line) for line in f if line.strip()]
|
|
587
|
+
|
|
588
|
+
if not items_data:
|
|
589
|
+
validation_errors.append("items.jsonl must contain at least one item")
|
|
590
|
+
else:
|
|
591
|
+
items_data = []
|
|
592
|
+
|
|
593
|
+
# Validate distribution.json
|
|
594
|
+
dist_file = experiment_dir / "data" / "distribution.json"
|
|
595
|
+
dist_data: dict[str, JsonValue] | None = None
|
|
596
|
+
if dist_file.exists():
|
|
597
|
+
with open(dist_file, encoding="utf-8") as f:
|
|
598
|
+
dist_data_obj: JsonValue = json.load(f)
|
|
599
|
+
if isinstance(dist_data_obj, dict):
|
|
600
|
+
dist_data = dist_data_obj # type: ignore[assignment]
|
|
601
|
+
|
|
602
|
+
if dist_data is None or "strategy_type" not in dist_data:
|
|
603
|
+
validation_errors.append(
|
|
604
|
+
"distribution.json must be a dict with strategy_type field"
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
# Additional validation checks
|
|
608
|
+
if check_distribution and dist_data:
|
|
609
|
+
_validate_distribution_config(
|
|
610
|
+
dist_data, validation_errors, validation_warnings
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
if check_trials:
|
|
614
|
+
_validate_trial_configs(
|
|
615
|
+
experiment_dir, validation_errors, validation_warnings
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
if check_data_structure and items_data and lists_data:
|
|
619
|
+
_validate_data_structure(
|
|
620
|
+
items_data, lists_data, validation_errors, validation_warnings
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
# Report results
|
|
624
|
+
if validation_errors:
|
|
625
|
+
print_error(f"Validation failed with {len(validation_errors)} error(s):")
|
|
626
|
+
for error in validation_errors:
|
|
627
|
+
console.print(f" [red]✗[/red] {error}")
|
|
628
|
+
if validation_warnings:
|
|
629
|
+
console.print()
|
|
630
|
+
console.print(
|
|
631
|
+
f"[yellow]⚠[/yellow] {len(validation_warnings)} warning(s):"
|
|
632
|
+
)
|
|
633
|
+
for warning in validation_warnings:
|
|
634
|
+
console.print(f" [yellow]⚠[/yellow] {warning}")
|
|
635
|
+
ctx.exit(1)
|
|
636
|
+
|
|
637
|
+
if validation_warnings:
|
|
638
|
+
console.print(f"[yellow]⚠[/yellow] {len(validation_warnings)} warning(s):")
|
|
639
|
+
for warning in validation_warnings:
|
|
640
|
+
console.print(f" [yellow]⚠[/yellow] {warning}")
|
|
641
|
+
|
|
642
|
+
print_success(
|
|
643
|
+
f"Experiment structure is valid "
|
|
644
|
+
f"({len(lists_data)} lists, {len(items_data)} items)"
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
except json.JSONDecodeError as e:
|
|
648
|
+
print_error(f"Invalid JSON in experiment data files: {e}")
|
|
649
|
+
ctx.exit(1)
|
|
650
|
+
except Exception as e:
|
|
651
|
+
print_error(f"Failed to validate experiment: {e}")
|
|
652
|
+
ctx.exit(1)
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def _validate_distribution_config(
|
|
656
|
+
dist_data: dict[str, JsonValue],
|
|
657
|
+
errors: list[str],
|
|
658
|
+
warnings: list[str],
|
|
659
|
+
) -> None:
|
|
660
|
+
"""Validate distribution strategy configuration.
|
|
661
|
+
|
|
662
|
+
Parameters
|
|
663
|
+
----------
|
|
664
|
+
dist_data : dict[str, JsonValue]
|
|
665
|
+
Distribution configuration data.
|
|
666
|
+
errors : list[str]
|
|
667
|
+
List to append errors to.
|
|
668
|
+
warnings : list[str]
|
|
669
|
+
List to append warnings to.
|
|
670
|
+
"""
|
|
671
|
+
strategy_type = dist_data.get("strategy_type")
|
|
672
|
+
|
|
673
|
+
# Validate strategy type
|
|
674
|
+
valid_strategies = [
|
|
675
|
+
"random",
|
|
676
|
+
"sequential",
|
|
677
|
+
"balanced",
|
|
678
|
+
"latin_square",
|
|
679
|
+
"stratified",
|
|
680
|
+
"weighted_random",
|
|
681
|
+
"quota_based",
|
|
682
|
+
"metadata_based",
|
|
683
|
+
]
|
|
684
|
+
|
|
685
|
+
if strategy_type not in valid_strategies:
|
|
686
|
+
errors.append(
|
|
687
|
+
f"Invalid strategy_type: {strategy_type}. "
|
|
688
|
+
f"Must be one of: {', '.join(valid_strategies)}"
|
|
689
|
+
)
|
|
690
|
+
return
|
|
691
|
+
|
|
692
|
+
# Validate strategy-specific configuration
|
|
693
|
+
strategy_config_raw = dist_data.get("strategy_config")
|
|
694
|
+
strategy_config: dict[str, JsonValue] | None = (
|
|
695
|
+
strategy_config_raw if isinstance(strategy_config_raw, dict) else None
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
if strategy_type == "quota_based":
|
|
699
|
+
if not strategy_config:
|
|
700
|
+
errors.append("quota_based strategy requires strategy_config")
|
|
701
|
+
elif "participants_per_list" not in strategy_config:
|
|
702
|
+
errors.append(
|
|
703
|
+
"quota_based requires participants_per_list in strategy_config"
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
elif strategy_type == "weighted_random":
|
|
707
|
+
if strategy_config and "weight_expression" not in strategy_config:
|
|
708
|
+
warnings.append(
|
|
709
|
+
"weighted_random without weight_expression uses uniform weights"
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
elif strategy_type == "metadata_based":
|
|
713
|
+
if not strategy_config:
|
|
714
|
+
errors.append("metadata_based strategy requires strategy_config")
|
|
715
|
+
elif (
|
|
716
|
+
"filter_expression" not in strategy_config
|
|
717
|
+
and "rank_expression" not in strategy_config
|
|
718
|
+
):
|
|
719
|
+
warnings.append(
|
|
720
|
+
"metadata_based without filter or rank expressions has no effect"
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
elif strategy_type == "stratified":
|
|
724
|
+
if not strategy_config:
|
|
725
|
+
warnings.append(
|
|
726
|
+
"stratified strategy without factors uses random assignment"
|
|
727
|
+
)
|
|
728
|
+
elif "factors" not in strategy_config:
|
|
729
|
+
warnings.append(
|
|
730
|
+
"stratified strategy without factors uses random assignment"
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
def _validate_trial_configs(
|
|
735
|
+
experiment_dir: Path,
|
|
736
|
+
errors: list[str],
|
|
737
|
+
warnings: list[str],
|
|
738
|
+
) -> None:
|
|
739
|
+
"""Validate trial configuration files if present.
|
|
740
|
+
|
|
741
|
+
Parameters
|
|
742
|
+
----------
|
|
743
|
+
experiment_dir : Path
|
|
744
|
+
Experiment directory.
|
|
745
|
+
errors : list[str]
|
|
746
|
+
List to append errors to.
|
|
747
|
+
warnings : list[str]
|
|
748
|
+
List to append warnings to.
|
|
749
|
+
"""
|
|
750
|
+
# Check for trial configuration files
|
|
751
|
+
config_dir = experiment_dir / "config"
|
|
752
|
+
if not config_dir.exists():
|
|
753
|
+
return # No config directory, skip trial validation
|
|
754
|
+
|
|
755
|
+
trial_configs = list(config_dir.glob("*_config.json"))
|
|
756
|
+
|
|
757
|
+
for config_file in trial_configs:
|
|
758
|
+
try:
|
|
759
|
+
config_data: JsonValue = json.loads(config_file.read_text(encoding="utf-8"))
|
|
760
|
+
|
|
761
|
+
if not isinstance(config_data, dict):
|
|
762
|
+
errors.append(f"Trial config {config_file.name} must be a JSON object")
|
|
763
|
+
continue
|
|
764
|
+
|
|
765
|
+
config_dict: dict[str, JsonValue] = config_data # type: ignore[assignment]
|
|
766
|
+
|
|
767
|
+
# Validate config type
|
|
768
|
+
config_type = config_dict.get("type")
|
|
769
|
+
if not config_type:
|
|
770
|
+
errors.append(f"Trial config {config_file.name} missing 'type' field")
|
|
771
|
+
continue
|
|
772
|
+
|
|
773
|
+
# Validate type-specific fields
|
|
774
|
+
if config_type == "rating_scale":
|
|
775
|
+
required_fields = ["min_value", "max_value", "step"]
|
|
776
|
+
for field in required_fields:
|
|
777
|
+
if field not in config_dict:
|
|
778
|
+
errors.append(
|
|
779
|
+
f"Rating config {config_file.name} missing '{field}' field"
|
|
780
|
+
)
|
|
781
|
+
|
|
782
|
+
elif config_type == "choice":
|
|
783
|
+
if "button_html" not in config_dict:
|
|
784
|
+
warnings.append(
|
|
785
|
+
f"Choice config {config_file.name} missing button_html "
|
|
786
|
+
f"(will use default)"
|
|
787
|
+
)
|
|
788
|
+
|
|
789
|
+
except json.JSONDecodeError:
|
|
790
|
+
errors.append(f"Trial config {config_file.name} contains invalid JSON")
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
def _validate_data_structure(
|
|
794
|
+
items_data: list[dict[str, JsonValue]],
|
|
795
|
+
lists_data: list[dict[str, JsonValue]],
|
|
796
|
+
errors: list[str],
|
|
797
|
+
warnings: list[str],
|
|
798
|
+
) -> None:
|
|
799
|
+
"""Validate JSONL data structure and schemas.
|
|
800
|
+
|
|
801
|
+
Parameters
|
|
802
|
+
----------
|
|
803
|
+
items_data : list[dict[str, JsonValue]]
|
|
804
|
+
Items data from items.jsonl.
|
|
805
|
+
lists_data : list[dict[str, JsonValue]]
|
|
806
|
+
Lists data from lists.jsonl.
|
|
807
|
+
errors : list[str]
|
|
808
|
+
List to append errors to.
|
|
809
|
+
warnings : list[str]
|
|
810
|
+
List to append warnings to.
|
|
811
|
+
"""
|
|
812
|
+
# Validate items structure
|
|
813
|
+
for i, item in enumerate(items_data):
|
|
814
|
+
if "id" not in item:
|
|
815
|
+
errors.append(f"Item {i} missing 'id' field")
|
|
816
|
+
|
|
817
|
+
if "item_template_id" not in item:
|
|
818
|
+
warnings.append(f"Item {i} missing 'item_template_id' field")
|
|
819
|
+
|
|
820
|
+
if "rendered_elements" not in item:
|
|
821
|
+
errors.append(f"Item {i} missing 'rendered_elements' field")
|
|
822
|
+
|
|
823
|
+
# Validate lists structure
|
|
824
|
+
for i, exp_list in enumerate(lists_data):
|
|
825
|
+
if "id" not in exp_list:
|
|
826
|
+
errors.append(f"List {i} missing 'id' field")
|
|
827
|
+
|
|
828
|
+
if "item_refs" not in exp_list:
|
|
829
|
+
errors.append(f"List {i} missing 'item_refs' field")
|
|
830
|
+
elif not isinstance(exp_list["item_refs"], list):
|
|
831
|
+
errors.append(f"List {i} 'item_refs' must be a list")
|
|
832
|
+
elif not exp_list["item_refs"]:
|
|
833
|
+
warnings.append(f"List {i} has no items (empty item_refs)")
|
|
834
|
+
|
|
835
|
+
# Check that all item_refs in lists exist in items
|
|
836
|
+
item_ids = {item.get("id") for item in items_data if "id" in item}
|
|
837
|
+
|
|
838
|
+
for i, exp_list in enumerate(lists_data):
|
|
839
|
+
if "item_refs" in exp_list and isinstance(exp_list["item_refs"], list):
|
|
840
|
+
# item_refs are UUID strings from JSON
|
|
841
|
+
item_refs_list = cast(list[str], exp_list["item_refs"])
|
|
842
|
+
for item_ref in item_refs_list:
|
|
843
|
+
if item_ref not in item_ids:
|
|
844
|
+
errors.append(f"List {i} references non-existent item: {item_ref}")
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
# Import nested command groups
|
|
848
|
+
from bead.cli.deployment_trials import deployment_trials # noqa: E402
|
|
849
|
+
from bead.cli.deployment_ui import deployment_ui # noqa: E402
|
|
850
|
+
|
|
851
|
+
# Register commands
|
|
852
|
+
deployment.add_command(generate)
|
|
853
|
+
deployment.add_command(export_jatos)
|
|
854
|
+
deployment.add_command(upload_jatos)
|
|
855
|
+
deployment.add_command(validate)
|
|
856
|
+
|
|
857
|
+
# Register nested command groups
|
|
858
|
+
deployment.add_command(deployment_trials)
|
|
859
|
+
deployment.add_command(deployment_ui)
|