ADR-17: Feature flag / cutover / rollback

Status: accepted (2026-05-29) Blocks: workstream E (bridge), workstream F (parity)

Context

Switching nirs4all’s backend from the legacy PipelineRunner to dag-ml is a high-blast-radius change. It must be reversible without reinstalls or data migration, and users must be able to validate parity on their own data before trusting the switch.

Decision

  1. Backend selectornirs4all.run(..., backend="legacy" | "dag-ml" | "dual"). Default is "legacy" until the G6 cutover gate, then the default flips to "dag-ml" in a minor release. The selector is also a workspace-level and env-level setting (NIRS4ALL_BACKEND).

  2. Dual-run modebackend="dual" runs both backends on the same input and diffs results within the ADR-01 tolerance ledger (per model class × metric). Mismatches beyond tolerance are reported, never silently reconciled. Dual mode ships for one release around cutover so users can validate on their own corpora.

  3. Legacy retention — the legacy backend stays available behind backend="legacy" for two releases past the default flip, then is removed under the ADR-14 managed-debt policy (removal version recorded in the CHANGELOG).

  4. Rollback — a user hitting a regression sets backend="legacy": no reinstall, no data migration, no bundle rebuild. Bundles produced by either backend stay loadable for predict for the retention window (ties to ADR-02’s bundle-readability SLA).

  5. Release notes — the default-flip release links the compatibility ledger (ADR-01) and the migration guide.

Consequences

  • nirs4all/api/run.py gains the backend parameter and dispatches to the legacy runner or the bridge.

  • Workstream F implements the dual-run diff against the tolerance ledger; the parity oracle (tests/integration/parity/) is the offline analogue.

  • The migration guide (G6 deliverable) documents the flag, dual-run validation, and the rollback procedure.

Risk

  • Dual-run doubles compute for the validation release. It is opt-in and intended for a one-release validation window, not steady state. The docs say so explicitly.