Skip to content

feat(schema): rivet schema migrate — git-rebase-style preset/version migration with abort/continue/skip #236

@avrabe

Description

@avrabe

Problem

Today rivet has no schema migration support. Users must hand-rewrite artifact YAML when:

  1. Switching presets — they started on dev (3 types) and outgrow into aspice (14 types) or stpa (10 types). Type renames, link-type renames, field shape changes — all manual.
  2. Upgrading a preset version — ASPICE 4.0 → 4.1 will be a real thing. There's no upgrade path.
  3. Refactoring overlays — adding/removing/renaming custom fields in a project-local overlay forces parallel rewrites of every affected artifact.

The existing-repo appendix in the quickstart says "redeclare every base field" — that's load-bearing advice. Right now there's no tooling to apply it mechanically. G.2 (overlay merge silently drops unlisted fields) is the trap that catches everyone.

Without migration tooling, the implicit user contract becomes "pick your preset on day 1, you're stuck with it." That contradicts the Path A → Path B story the rewritten quickstart promises.

Proposed solution

rivet schema migrate — a git rebase-style state machine for applying a structural rewrite to all artifacts in a project, with explicit support for pausing on conflicts.

Three classes of change (the rebase pick / edit / drop)

Class Examples Auto-resolve?
Mechanical requirementsw-req; satisfiesderives-from; enum value re-spelling (functionalfunctional-req) YES — diff source/target schemas, generate a rewrite map
Decidable with policy Source field doesn't exist on target. Default: drop. With --keep-as-orphan: stash under fields.legacy.*. With --strict: fail YES with explicit policy flag
Conflicting Source has priority: 5 (number); target requires priority: must (enum). Or: required link target type changed from [any] to [stakeholder-req] and current link points elsewhere NO — needs user input

CLI surface

```bash

1. Plan (default = dry-run)

rivet schema migrate aspice

Reads current schemas + target preset

Writes .rivet/migrations//plan.yaml

Prints human summary: X auto, Y needs-policy, Z conflicts

Exits 0 even with conflicts (plan succeeds; apply blocks)

2. Apply mechanical changes; pause on first conflict (rebase semantics)

rivet schema migrate --apply

Walks plan in order. For each artifact:

- If auto-mappable: rewrite the file

- If conflict: write conflict markers into the YAML, pause

Exits non-zero if paused; exits 0 if fully applied

3. Conflict resolution flow (the rebase-interactive equivalent)

rivet schema migrate --status

Shows: applied N of M, current conflict on , action options

rivet schema migrate --continue

Validates the user resolved the current conflict

(no <<<<<<< markers remain in the affected file)

Resumes the walk to the next conflict or completion

rivet schema migrate --skip

Skip the current artifact (drop it from the migration);

the original is preserved unchanged — like rebase --skip

rivet schema migrate --abort

Restore from .rivet/migrations//snapshot/ — full pre-migration state

Like git rebase --abort

rivet schema migrate --edit

Re-open the conflict block on a previously-resolved artifact

for re-editing before --continue

4. After completion

rivet schema migrate --finish

Validates with rivet validate, deletes the snapshot

(snapshots are persisted across sessions to support multi-day migrations)

```

State machine

```
(no migration)

▼ rivet schema migrate
[PLANNED]

▼ --apply
[IN_PROGRESS]
┌───┴───┐
│ │
(no conflict) (conflict found)
│ │
│ ▼
│ [CONFLICT] ◄──┐
│ │ │
│ │ --continue (after edit)
│ │ --skip
│ │ --edit
│ │ --abort ────► [PLANNED] then [IDLE]
│ │
│ └─────────┘

[COMPLETE]
│ --finish

[IDLE]
```

Storage layout

```
.rivet/migrations/
└── 20260428-1900-dev-to-aspice/
├── plan.yaml # the full diff: per-artifact, per-field action
├── manifest.yaml # current state machine pointer + per-artifact status
├── state # one of: PLANNED | IN_PROGRESS | CONFLICT | COMPLETE
├── current-conflict # artifact id currently paused on (if state == CONFLICT)
└── snapshot/ # full pre-migration artifact YAML tree (for --abort)
└── artifacts/
└── ...
```

The directory survives across sessions — a multi-day migration is fine. Only one migration in flight at a time per project (later migrations error with "another migration is in progress").

Conflict markers in YAML (merge-conflict style)

When --apply hits a conflict, it writes the affected artifact's field as:

```yaml

  • id: REQ-001
    type: sw-req # was: requirement (auto-renamed)
    fields:
    priority: <<<<<<< source: dev (priority: number)
    5
    ======= target: aspice (priority: enum [must|should|could|wont])

```

User edits the YAML, removes the conflict markers, runs rivet schema migrate --continue. The continue handler verifies no markers remain, runs rivet validate on just the touched artifact, then proceeds.

What ships in binary vs as recipes

  • In binary: diff engine (two schemas → rewrite map), conflict-marker writer, state machine plumbing, auto-policies (drop, keep-as-orphan, strict).
  • As recipes (schemas/migrations/<source>-to-<target>.yaml, hand-curated): named pre-baked migrations for common preset pairs (devaspice, aspicescore, stpastpa-sec, version bumps for the same preset). Used when the binary's auto-mapping needs human judgment.

Recipe shape:

```yaml

schemas/migrations/dev-to-aspice.yaml

migration:
name: dev-to-aspice
source: { preset: dev }
target: { preset: aspice }
description: |
Mechanical mapping for the most common dev → aspice transition.

type-rewrites:
- from: requirement
to: sw-req
field-map:
priority: priority # same name, but enum values constrained
# ... explicit per-field decisions

- from: feature
  to: sw-arch-component
  # human note in PR review: "this is a judgment call —
  # could also be sys-arch-component for hardware-bound features"

- from: design-decision
  to: design-decision   # same name in both; identity rewrite

link-rewrites:
- from: satisfies
to: derives-from

policies:
unmapped-fields: keep-as-orphan
unmapped-link-types: drop
```

Test strategy

  1. Roundtrip property: for every reversible migration (A → B where there's also a B → A recipe), running migrate(A→B) then migrate(B→A) must yield artifact YAML byte-identical to the original (modulo formatting). Catches asymmetric mapping bugs.
  2. Validation gate: every recipe ships with a fixture (a small valid project in source schema). Test runs rivet init --preset <source>, populates with the fixture, runs migrate --apply, asserts rivet validate passes on the result.
  3. Conflict harness: handcrafted "hard" projects that intentionally contain every conflict class. Test runs migrate, expects exact set of conflicts, then runs --continue after pre-baked resolutions.
  4. Abort symmetry: migrate --apply --abort must leave the project byte-identical to before. Snapshot diff test.

Open questions

  1. Multi-overlay interaction. If a project has [common, aspice, legacy-overlay] schemas registered, and the user runs migrate score, does the overlay get dropped, kept-as-orphan, or migrated separately? Probably: overlays remain, but get an automatic extends: retarget if their parent type was renamed. Document the rule.
  2. Dashboard surface. Should there be a /migrations/<id> dashboard page showing the in-flight migration? MVP says no (CLI-only). Post-MVP: yes — render the per-artifact diff with click-to-resolve.
  3. Migration recipe distribution. Recipes ship with the binary today. As the recipe set grows, do we want them as a separate crate / pulled from a remote registry? Probably out of scope for v1.
  4. Schema-version bumps within the same preset. ASPICE 4.0 → 4.1 looks like a "type-rewrites: []" recipe with only field-level changes. The recipe format above already supports this; needs to be exercised in tests.
  5. Provenance trail. Each migrated artifact should carry an entry in its provenance: field recording the migration (timestamp, recipe used, source preset). Out of scope for MVP but worth mapping.
  6. Cross-repo coordination. If a project uses cross-repo links (rivet externals), and the local schema migrates but the externals don't, what happens? Document: cross-repo links are by ID and don't carry type info, so they're transparent to local migration. But: if both repos are migrating, coordination is the user's problem in v1.

MVP scope (v0.6.0 marquee)

Phase 1 (~2 weeks):

  • The diff engine + the rewrite-map computation
  • rivet schema migrate <target> (plan only, dry-run)
  • rivet schema migrate --apply for mechanical-only migrations (no conflict resolution yet — just bail loudly if conflict)
  • --abort (full snapshot rollback)
  • One canned recipe: dev-to-aspice with full test coverage
  • Property test: roundtrip on the canned recipe

Phase 2 (~2 weeks): the rebase-style state machine

  • Conflict markers in YAML
  • --continue / --skip / --status
  • The state file + snapshot persistence
  • Conflict harness test

Phase 3 (post-MVP):

  • --edit <id> (re-open resolved conflict)
  • Dashboard surface
  • Recipe distribution (probably a rivet recipes subcommand)
  • Provenance entries

Trigger

Surfaced during the post-0.5.0 fresh-user dogfood: scenario B (Polarion → ASPICE bring-up) explicitly hand-wrote an overlay because the binary couldn't help. The compliance-lead persona — exactly the audience rivet's three-pillar pitch targets — is the one most affected. Today they spend 5–7 minutes on an overlay; with migrate they should spend 30 seconds on a recipe lookup.

Acceptance

  • rivet schema migrate dev-to-aspice (plan-only) on a dev project lists every type/link/field rewrite with the correct action class
  • --apply on a conflict-free project produces a rivet validate-clean output in target preset
  • --abort restores byte-identical pre-migration state
  • --continue / --skip walk through conflicts correctly
  • At least one canned recipe (dev-to-aspice) ships with documented round-trip property test
  • rivet docs schema-migrations documents the state machine + conflict marker format
  • rivet docs check recognizes <<<<<<< markers in artifact YAML and lists them as a MigrationConflict invariant

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions