3. Generating EML from Primitives

This page defines the authoring and compilation layer before EML. A Python script ingests experiment primitives and design rules, validates them, expands them into concrete trials, materializes every sequence, and writes the static EML package described in 2. EML → Rendering.

Scope of this page: primitives, asset and query metadata, model cards, trial generation, conditions, blocks, sequencing, compiler inputs, and provenance. Renderer behavior is documented separately.

3.1. Primitives → EML

A build manifest, self-describing trial primitives, instructions, and experimental design rules enter a Python compiler, which writes a static EML package.
Compiler guarantee: every variant, trial ID, condition, block, sequence, seed-dependent choice, and build-time function is resolved before output. Deployed EML contains data, not executable authoring logic.

3.2. Compiler Inputs

ObjectQuestion it answersContents
eml_build_card.jsonHow should this experiment be compiled?Build manifest: generator entrypoint and version, source path, seed, output paths, and build-time contract.
experiment_source.jsonWhat experiment should be compiled?Experiment metadata, self-describing trial primitives with stimulus and query definitions, instructions, blocks, and sequencing intent.
trial_units[].model_cardWhat is this primitive and how may it be used?Primitive name and version, description, intended use, provenance, and stimulus/query/feedback contract.

3.3. Build Manifest

eml_build_card.json is the compiler manifest. It identifies the Python generator, input document, reproducibility settings, expected outputs, and build-time limitations. It is separate from the model card carried by each primitive and is not loaded by the experiment renderer.

{
  "card_type": "eml-build-card",
  "eml_version": "1.5",
  "name": "Ball comparison example",
  "source": "experiment_source.json",
  "generator": {
    "entrypoint": "build_eml.py",
    "version": "1.0.0",
    "language": "python>=3.10"
  },
  "reproducibility": {
    "seed": 17,
    "sequence_materialization": "explicit"
  },
  "outputs": {
    "directory": "generated_eml",
    "config": "config.json",
    "trials": "trial.jsonl",
    "instructions": "instruction.jsonl",
    "readme": "README.md",
    "provenance": "build_provenance.json"
  }
}
Card fieldPurpose
sourceThe experiment source consumed by the Python builder; it contains all primitive model cards, definitions, instructions, and sequencing intent.
generatorEntrypoint, version, and language requirement for the compilation logic.
reproducibility.seedSeed used for any build-time shuffling. The resulting orders are materialized in config.json.
outputsNames and location of the generated EML package files.
contractOptional declarations such as build-time-only execution and static feedback.

3.4. Experiment Source

experiment_source.json holds the experiment-wide authoring inputs and every self-describing primitive. It is a compiler input, not a replacement for the original EML files.

SectionCompiled result
metadata and trial_layout_configCopied into config.json; stimuli_count is calculated.
trial_unitsEach unit's model card is validated; its conditions are expanded into concrete records in trial.jsonl.
sequencingResolved into explicit conditions, sequences, and blocks in experimentFlow.
instructionsWritten one object per line to instruction.jsonl.

3.4.1. Primitive model card and trial unit

The primitive participant-facing unit is stimulus → query → feedback. Every trial unit must include a model_card before those executable fields. The card documents the primitive's identity, version, intended use, provenance, and component contract. The rest of the unit names stimulus conditions, query objects, optional static feedback, and the rules for generating concrete trial IDs.

{
  "id": "trial_1",
  "model_card": {
    "name": "Apparent circle-size comparison",
    "version": "1.0.0",
    "description": "Presents one condition and asks which circle appears larger.",
    "intended_use": "Generate visual-perception comparison trials.",
    "provenance": {
      "source": "CogGym EML v1.5 documentation example",
      "license": "CC BY 4.0"
    },
    "contract": {
      "stimulus": "one named condition from each stimulus definition",
      "query": "one or more named response objects",
      "feedback": "optional static post-response content"
    }
  },
  "stimuli": {
    "stimulus_1": {
      "base": {"input_type": "text", "text": "The circles appear equal."},
      "variant_1": {"input_type": "text", "text": "The blue circle appears larger."},
      "variant_2": {"input_type": "text", "text": "The red circle appears larger."}
    }
  },
  "queries": {
    "query_1": {
      "prompt": "Which circle appears larger?",
      "type": "multi-choice",
      "tag": "circle_size",
      "option": ["Blue", "Red", "Equal"],
      "required": true
    }
  },
  "generation": {
    "vary": [
      {"stimulus": "stimulus_1", "conditions": ["base", "variant_1", "variant_2"]}
    ],
    "queries": ["query_1"],
    "id_template": "{unit}_{stimulus_1}"
  },
  "feedback": {
    "type": "text",
    "content": "Your response has been recorded."
  }
}

The Python builder validates the card, then produces trial_1_base, trial_1_variant_1, and trial_1_variant_2. Static feedback and delay, when present, are copied to every generated trial. The model card remains authoring/provenance metadata and is not serialized into trial.jsonl.

3.4.2. Trial generation and sequencing

The source JSON describes how the builder should construct sequences from generated unit IDs. The builder resolves every strategy immediately; config.json contains only explicit trial IDs.

{
  "sequencing": {
    "conditions": [
      {
        "condition": "all_variants",
        "prefix": ["instruction_01"],
        "sequences": [
          {
            "seq_id": "seq_forward",
            "units": ["trial_1"],
            "order": "generated",
            "block_size": 3
          },
          {
            "seq_id": "seq_reverse",
            "units": ["trial_1"],
            "order": "reverse",
            "block_size": 3
          }
        ]
      }
    ]
  }
}

The reference builder supports generated, reverse, and seeded shuffle order. Experiment-specific Python may implement other counterbalancing or sampling rules, provided it writes concrete sequences that follow the original experimentFlow schema.

3.5. Compilation and Outputs

python3 build_eml.py eml_build_card.json
  1. Read the build manifest and complete experiment source.
  2. Validate every primitive model card and component contract.
  3. Expand every trial unit and reject duplicate generated IDs.
  4. Generate each requested sequence from the expanded unit IDs.
  5. Materialize original EML files and calculate stimuli_count.
  6. Write primitive card identities, hashes, generator version, seed, trial IDs, and sequence IDs to build_provenance.json.
generated_eml/
├── README.md
├── config.json
├── trial.jsonl
├── instruction.jsonl
└── build_provenance.json

The runnable example includes eml_build_card.json, experiment_source.json, build_eml.py, and the generated EML package.