seihou-cli-0.4.0.0: help/migrations.md
MIGRATIONS
A migration is an author-declared sequence of file-system operations that
moves a project's working tree from one module version to another. When a
module is upgraded — say, from haskell-base 1.0.0 to 2.0.0 — the project
that has it applied may need to rename directories, drop files that v2
doesn't ship, or run a one-off command. Migrations let module authors
ship that knowledge inside `module.dhall` so consumers don't have to read
a CHANGELOG and reconcile by hand.
WHEN TO AUTHOR A MIGRATION
Add a migration when a new version of your module changes the *layout*
of the files it generates: a directory rename, a file removal, a path
pattern shift. You do not need a migration for content-only changes;
re-running `seihou run <module>` already updates file content. Use
migrations specifically for things that `seihou run` cannot infer on
its own.
THE MIGRATION RECORD
Each migration is one record in the `migrations` field of `module.dhall`:
, migrations =
[ S.Migration::{ from = "1.0.0"
, to = "2.0.0"
, ops =
[ S.MigrationOp.MoveDir
{ src = "app", dest = "src" }
, S.MigrationOp.DeleteFile
{ path = "Setup.hs" }
]
}
]
- `from` and `to` are dotted versions parsed by `parseVersion`.
- `ops` is the list of operations applied in order.
OPERATIONS
Authors compose migrations from five typed operations:
MoveFile Rename a single tracked file. The engine rewrites the
manifest's files-map key from src to dest so subsequent
`seihou status` and `seihou diff` keep working.
S.MigrationOp.MoveFile { src = "lib/Old.hs", dest = "src/New.hs" }
MoveDir Rename a directory. Every manifest entry whose path lies
under src/ has its key rewritten with the dest/ prefix.
S.MigrationOp.MoveDir { src = "app", dest = "src" }
DeleteFile Remove a tracked file. Drops the manifest entry.
S.MigrationOp.DeleteFile { path = "Setup.hs" }
DeleteDir Recursively remove a directory and drop every manifest
entry under that prefix.
S.MigrationOp.DeleteDir { path = "obsolete" }
RunCommand Run a shell command. The escape hatch for changes the
other ops can't express. The manifest is *not*
automatically updated; if your command moves files, you
are responsible for following it with explicit
MoveFile/DeleteFile ops or living with manifest drift.
S.MigrationOp.RunCommand
{ run = "cabal run my-tool -- migrate", workDir = None Text }
WINDOW SEMANTICS
When a user runs `seihou migrate`, the planner walks every declared
migration whose [from, to] range falls inside [installed, target] in
ascending `from` order, advancing a cursor as it goes. Migrations
whose `from` is already past the cursor (overlap or stale) are
skipped silently; migrations whose `to` exceeds the target are
skipped (a future invocation with a higher target will pick them
up).
The cursor never has to land on a declared `from`. Gaps are
permitted:
installed: 0.2 target: 0.6
declared: 0.2 → 0.3, 0.5 → 0.6
Both edges run in order. The 0.3 → 0.5 gap has no declared
migration and the planner does not stop — it skips the gap and
keeps walking.
MANIFEST ALWAYS LANDS AT TARGET
After a successful (non-dry-run) migration, the manifest's recorded
version for the module advances to the supplied target — regardless
of whether any of the declared migrations bridged every gap. A plan
with zero in-window migrations is a "pure version bump" that
advances the manifest's `moduleVersion` field without running any
ops. The terms "blocked migration", "benign upgrade", and
"bump-through" are gone; every invocation either has nothing to do
(`installed == target`) or lands the manifest at the target.
DUPLICATES AND OVERLAPS
If two migrations share the same `from`, that is an ambiguity and
the planner refuses (`MigrationDuplicateEdge`). Authors who want a
"leapfrog" migration alongside the smaller steps should declare the
longer span and let the smaller overlapping edges sit unused — the
cursor will advance past them after picking the leapfrog.
CONFLICT SEMANTICS
Mirroring `seihou remove`, the engine classifies each
file-targeted op into one of three states before touching disk:
MFSafe Disk hash matches the manifest's recorded hash. Free
to move or delete.
MFConflict Disk hash differs. The user has edited the file since
it was generated. The engine refuses to overwrite
unless `--force` is passed.
MFGone File is absent on disk. The op is a no-op.
The conflict check happens up front. With one or more `MFConflict`
paths and no `--force`, no disk mutation occurs. With `--force`, the
migration proceeds and the user-edited bytes ride along (a
MoveFile preserves content; a DeleteFile drops it).
DRY RUN
`seihou migrate <module> --dry-run` prints the plan and exits. The
output is identical to a real run, minus the actual disk
modifications. Use this before any non-trivial migration to see
what would be touched.
For a machine-readable form, pass `--json` (works alongside
`--dry-run`).
UPGRADE INTEGRATION
`seihou upgrade` updates the central installed copy under
~/.config/seihou/installed/<name>/ but does *not* rewrite project
trees by default. After an upgrade that brings new migrations,
`seihou upgrade` prints a one-line advisory:
note: haskell-base has 1 migration(s) pending (1.0.0 → 2.0.0);
run 'seihou migrate haskell-base'
Pass `--with-migrations` to skip the advisory and run migrations
for each upgraded module against the current project in one shot.
STATUS INTEGRATION
`seihou status` shows a `Pending migration: <from> -> <to> (<N>
step(s)). Run: seihou migrate <name>` sub-line under any applied
module whose installed copy has advanced past the manifest's
recorded version. `<N>` is the count of in-window declared
migrations that will run; it may be zero (the version range has no
applicable migrations and the run will only advance the manifest).
The line is informational — use `seihou migrate <module>` to apply.
MANIFEST GUARANTEE
After a successful (non-dry-run) migration, the manifest's files map
reflects the new paths exactly: a MoveFile rewrites one key, a
MoveDir rewrites every contained key, deletes drop their entries,
and the manifest's `genAt` is bumped. The named applied module's
`moduleVersion` is updated to the supplied target (which may differ
from the highest `to` in the applied migrations — see "Manifest
always lands at target" above). `seihou diff` after a clean
migration is empty; `seihou status` shows no pending migrations.
SEE ALSO
seihou migrate --help
seihou upgrade --help
docs/user/migrations.md