How validation works#

Func diagnostics#

Model-function names in the pipeline are checked against the schema itself (the single source of truth), not a separate list, so every stage is covered. When a func is wrong, validate_config now:

  • collapses the schema’s downstream argument explosion (one bad func could otherwise surface hundreds of misleading “X is a required property” errors from the branches the validator tried) down to a single diagnostic;

  • distinguishes a typo from a valid model in the wrong stage, and points the latter at the stage it belongs to;

  • offers a “did you mean” suggestion (Levenshtein over the leaf name) and a short list of valid models for that stage, carried as structured suggestion and valid_options fields on the Diagnostic.

This is the Python port of the browser GUI’s modelDiscriminator.js, so both consumers reason about func values the same way.

What validation covers#

The core validates what the schema encodes, plus a short list of run rules for what Pyxel refuses beyond it; both are shared with the GUI, so the two reach the same verdict. The schema already includes a fair amount of value checking, not just structure:

  • types and required arguments per model;

  • enums — a bad choice string is rejected against the allowed set (e.g. single / exponential / binomial / sbx);

  • numeric bounds (minimum / maximum / exclusiveMinimum) and array-length bounds (minItems / maxItems) wherever the schema declares them;

  • func discriminators, with the friendly typo / wrong-stage diagnostics described above;

  • the root shape: which running mode and which detector the config uses. The schema’s root is an anyOf over twelve (mode, detector) branches, and a bare validator answers a missing or misspelled detector with the same “required property” line four times over. The core reads the branch structure out of the schema (the Python port of the browser app’s configShape.js) and reports one sentence instead: which section is missing, which two conflict, or that ccd_detecor should be ccd_detector.

  • run rules (run_rules.py, mirrored by the browser’s runRules.js): what Pyxel 3.0.2 refuses at load or stops on at run time although the schema accepts it. A readout with both times and times_from_file, a calibration with both weights and weights_from_file, an observation with no parameters, simple_measurement with neither a gain nor a detector charge_to_volt, simple_amplifier without pre_amplification, and simple_adc without its ADC settings, a whole number written as a decimal where Pyxel refuses one (a detector’s row: 128.0), a number in exponent notation that Pyxel’s YAML reader takes as text (1e-6), and a model whose own code refuses the config’s detector class (cdm on a CMOS detector). They report under the pyxel-rule category, and CI runs a fixture for each one through the real Pyxel so a rule cannot drift from what Pyxel does.

  • duplicate keys: Pyxel keeps the last of two equal keys in a mapping without a word, so the core refuses a key written twice and names both lines.

  • focal plane arrays (fpa.py): the schema has no focal_plane_array branch, so a plane is checked against what Pyxel’s focal-plane loader requires (one running mode, exposure only; the physical geometry and one scale; tile IDs 1..N; positions; a plane-wide scene; nothing beside the plane), and each tile is validated with the schema and the run rules as the flat config Pyxel builds it from: the plane’s running mode, the tile’s detector and the tile’s pipeline. Findings name the tile and keep their path inside the plane, so the line in a SARIF or Code Quality report is the line in the file.

Reports are grouped by path: one field’s alternatives collapse into one line naming every accepted type, at most eight paths are listed, and a closing line counts what was left out so nothing is dropped silently. A pipeline step whose func is known is checked against that model’s own definition, so a mistyped argument reports as itself and not as the required arguments of every other model in the stage.

Deliberately out of scope#

Beyond the run rules above, checks that live above the schema are not attempted here, so there is never a second rulebook that could drift from the GUI or from Pyxel:

  • Other super-schema constraints. Cross-field consistency (e.g. one argument that must stay <= another), physical plausibility, and any other bound Pyxel enforces in code but that isn’t written into the JSON schema. These pass the schema check. The --deep switch exists for exactly this gap: it hands the config to pyxel.load(), which applies every rule Pyxel has at load time.

The shared contract#

Bundled under pyxel_config_core/data/:

  • pyxel_schema.json: the validation contract

  • modelParameters.json: model parameters and defaults

  • tutorials/: the tutorial catalogue

  • BUNDLE.json: the manifest, naming the Pyxel release the schema was generated from, its SHA-256, and when. schema_version() reads it, every validation result carries it, and pyxel-config doctor prints it along with whether the shipped schema still matches the digest.

These are copied from the Config Lab repo by one command, run from the repo root: python scripts/refresh_bundle.py rewrites every artifact from the installed Pyxel (both schema copies, the metadata snapshot, the precompiled validator, the tutorial facts, the package’s tutorial copies, and the manifest), and python scripts/refresh_bundle.py --check fails in CI when any of them is stale. Rebuild the vector index (pyxel-config index) whenever the tutorials change.