Skip to content

Versioning

The SSVEP Toolbox follows Semantic Versioning 2.0.0 from 1.0.0 onward, as CHANGELOG.md already states. This page says precisely what that guarantee covers, because a manifest-spine project like this one has more than one thing that could be called "the API."

Pre-1.0

Before 1.0.0, anything may change between minor versions — the version number is informative, not a compatibility promise. 1.0.0 is the first version SemVer applies to.

What SemVer covers from 1.0.0

The public Python API. The covered surface is exactly what the API reference renders (docs/api/, one page per subpackage) — the modules `-included there, and only the public (non-underscore-prefixed) names within them. A breaking change to it is a major version bump, a backward-compatible addition is minor, a fix is patch.scripts/check_page_content.pyalready asserts every public, docstringed top-level object in each of those modules renders on the page, so "what the API reference renders" is a checked set, not an aspirational one. A public module that exists but is not yet included in the API reference — today, for example,ssvep.io.protocol,ssvep.io.export,ssvep.io.xdf,ssvep.spatial,ssvep.windowing,ssvep.runtime.discovery,ssvep.analysis.resting_paf` — is not covered by this promise until it is added there; being importable without a leading underscore is not by itself a compatibility commitment.

Console/GUI entry points (ssvep-toolbox, ssvep-design, ssvep-run, and BIDS/code/*.py) are not part of the covered Python API even though they are documented commands: they are launchers and analysis scripts, not importable modules rendered in the API reference. They follow the "not covered" rules below (GUI layout/wording), the same as anything under scripts/.

The manifest, protocol, site-profile, and marker JSON Schemas (schemas/*.schema.json) — the run manifest, the session protocol, the site profile, and the recorded-marker stream. Each of the first three carries its own version field (for example protocol_version), independent of the package's own version, because a manifest saved under an old package version must keep loading and keep fingerprinting exactly as it did when it was written — that is the manifest-spine guarantee (AGENTS.md §2.2), and SemVer here is in service of it, not a replacement for it. Concretely:

  • A schema version that only adds an optional field, in a way every existing saved file still validates against and still fingerprints identically, is a compatible change.
  • A schema version that changes what a previously valid file means, or requires a new field on every file, is breaking.

Old, already-saved manifests and protocols are never broken retroactively by a version bump. The session protocol and the run manifest have so far only ever added optional fields across every version to date (1.0 through 1.5), so no rewrite-on-read has ever been needed for either one; ssvep.io.manifest.canonicalize_manifest exists for the day one is (it already remaps the two legacy manifest spellings a saved file can carry). If a session-protocol change ever needs the same kind of remap, it belongs beside canonicalize_manifest, not as a new mechanism.

schemas/markers.schema.json — the LSL marker-stream contract every recording carries — has no version field of its own, unlike the other three. It is still covered by this policy (additive fields are compatible, a change to what an existing field means is breaking), but a breaking change to it has no old-file migration question the way a manifest or protocol does: markers are read live, by their structured fields, off the stream a run emits, never replayed later against a stored schema version. So a breaking change here is tracked as a plain package-level SemVer bump: the next recording's stream disagreeing with older analysis code is exactly what a major bump communicates.

What "experimental" means

This project's pre-release schema freeze is 2026-10-21. A schema addition that lands after that date, without having gone through a full round of design review and real-recording use, ships marked experimental in its own schema description and in the changelog entry that introduces it. An experimental field is present and usable, but it sits outside the SemVer compatibility guarantee — outside it until 1.1, at the earliest: it may still change shape, be renamed, or be removed in a later minor version, without that counting as a breaking change under this policy. It graduates — and starts being covered — after it has shipped a full cycle (one minor release) without needing to change shape.

This is a deliberate trade: holding 1.0.0 for every open schema question would either delay the release indefinitely or lock in a design nobody has used yet. Marking the newest, least-exercised surface as experimental lets the rest of the schema make its compatibility promise on schedule.

Currently experimental

  • Manifest v1.4's image-target block (stimulus pattern: "image" plus the per-stimulus image object, schemas/run_manifest.schema.json, #256–#259) landed ahead of the 2026-10-21 freeze but is carried as experimental regardless — it has not yet completed a full release cycle of real-recording use — and is experimental today.
  • Manifest v1.6's design.feedback block and the feedback marker segment (schemas/run_manifest.schema.json, schemas/markers.schema.json, #268) add participant feedback after each trial. They landed after the freeze and have not shipped a release cycle, so they are experimental. Level 1 (a check mark or cross on the cued target) is built. Level 2 (manifest v1.7) adds the selection mode (a frame on the decoded target), design.cue_mode (free: the participant chooses the target and no label is recorded) and a marker contract in which target_freq_hz is required unless the marker says cued: false. Those are experimental too; growing this surface is the kind of change this status exists to allow.
  • Manifest v1.8's design.early_stop block (schemas/run_manifest.schema.json, #23) declares live early stopping: the frozen threshold, the growing-window schedule (its maximum is design.timing.stim_s) and where the threshold came from. It landed after the freeze, so it is experimental, and so are the surfaces that run it: the "stop" message on the ssvep_feedback back-channel (answers now carry "kind": "answer"; a message without kind is still an answer) and the stim_off marker fields presented_s, stopped, decided_window_s, margin and stop_latency_s (schemas/markers.schema.json). The analysis of live-stopped runs is experimental too: decision_rule.applied, the live_stop, window_sweep_truncated, snr_window_s and snr_window_note keys of metrics.json, trial_outcomes.elapsed_s, the contrast factor stopping, and the five early-stopping events.tsv columns.

The release plan also expects the following to land as schema changes before 1.0.0's freeze point and to ship experimental under this same rule; each is a forward-looking schema surface, not yet in schemas/*.schema.json:

  • #31 — multi-condition (factorial/sweep) run declarations for contrasts, extending ssvep.analysis.synthetic's existing sweep support into the schema itself.
  • #26 — a schema representation for electrode carriers driven by the planned montage editor (where user-defined carriers live is still an open question per the release plan).

Not covered by SemVer at all

  • The GUI's layout, wording, and keyboard shortcuts.
  • Console/GUI entry points' argument syntax and output, and anything under BIDS/code/ — see above.
  • Anything under scripts/ (developer-only tooling, not installed as part of the package).
  • Log/console message text.
  • The exact numeric output of an analysis for a given input, beyond what the project's own tests pin — a bug fix that changes a previously-wrong number is a fix, not a break, and is recorded in CHANGELOG.md either way.