Files
msd-core/docs/adr/0008-installer-migration-module.md
Jakub Zych a9a7a328e6 refactor: hard-fork GSD -> MSD (Make Software Done)
Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD
across contents and paths, upstream package/repo coordinates -> @golem15/msd-core
and golem15com/msd-core. Deep links into upstream history, sibling upstream
packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is.

Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line,
package/plugin identity, regenerated lockfile, install-tree fixtures, derived
registries and benchmark baseline; migration checksum baseline re-locked
(MSD keeps its own install state, so no install had applied the old sums);
sort-order and regex-escaped expectations in tests adjusted.
2026-10-06 01:47:40 +02:00

96 lines
5.7 KiB
Markdown

# Installer Migration Module owns install-time upgrade safety
- **Status:** Accepted
- **Date:** 2026-05-11
We decided to introduce an explicit Installer Migration Module for install-time file moves, removals, config rewrites, and user-data preservation. Installer upgrade behavior must be represented as versioned migration records that produce a dry-run plan before applying changes.
## Decision
- Add an Installer Migration Module as the owner for upgrade migrations.
- Keep the existing installer materialization pipeline, but move cleanup and feature-retirement behavior into migration records over time.
- Track applied migrations in an install-state file next to the existing file manifest.
- Treat the existing file manifest as the managed-file ownership baseline.
- Treat user-owned artifacts as a single shared policy consumed by preservation and manifest writing.
- Require migrations to plan first, then apply through a shared executor that owns backup, rollback, and reporting.
- Default ambiguous or unknown files to preserve; destructive changes need managed-file evidence or explicit user choice.
- Support dry-run output using the same planner used by apply mode.
- Include a first-time baseline scanner for legacy installs that need classification before destructive migrations can be trusted.
- Treat the runtime configuration contract registry in `docs/installer-migrations.md` as the source of truth for migrations that touch host runtime config.
## Runtime Contract Decision
Every migration that rewrites runtime config, moves an invocation surface, or
retires a generated runtime artifact must cite the registry row in
`docs/installer-migrations.md`. If the migration changes where a runtime loads
commands, skills, agents, hooks, or rules, the PR must update both the registry
and `docs/ARCHITECTURE.md`.
The registry records what MSD installs, where it installs it, when migrations
may touch it, who owns the surrounding config, and why the shape matches the
host runtime. When upstream docs do not publish an API or docs version, the
checked date is the drift sentinel. A later upstream docs or CLI release that
changes command, skill, agent, hook, or rule loading requires a new registry
snapshot before migration work proceeds.
## Consequences
- Retiring features requires an explicit migration instead of a hidden cleanup block.
- The installer can remove stale MSD-owned artifacts without guessing about user files.
- Locally modified managed files get a consistent backup path before removal or replacement.
- Future rollback work can become runtime-neutral instead of Codex-specific.
- Migration authors must define ownership evidence, conflict behavior, runtime scope, and non-interactive behavior.
- Migration authors must also define which runtime contract they are relying on and whether the upstream documentation is versioned.
- The installer gains another state file, so tests must cover missing, legacy, and checksum-mismatch state.
## Scope
The first implementation should extract manifest/user-owned helpers, add install-state persistence, add migration planning, and port one existing orphan cleanup into the migration runner. It should not rewrite every runtime installer branch in the first pass.
The detailed module contract lives in `docs/installer-migrations.md`.
## Amendment (2026-05-11): Authoring guard enforcement
The Installer Migration Authoring Guard Module validates migration records and
planned actions before planning can proceed. Records must declare title,
description, introduction version, explicit install scopes, destructive status,
and a plan function. Destructive or config-rewrite actions must include
ownership evidence, and runtime config rewrites must cite the runtime
configuration contract registry.
## Amendment (2026-08-07): Non-recursive empty-directory removal primitive
Migration 003's docblock records, as an intentional consequence of this ADR,
that the framework has no *recursive* directory-removal primitive: every
action targets a single file by `relPath`, and an emptied directory shell is
left behind for the user (or a future migration) to clean up. #3023 exposed a
case where that is not enough: pi reserves the directory NAME `hooks/` for its
own deprecated-extension check, which warns on the path's mere existence
regardless of contents. Leaving an emptied `hooks/` shell behind would keep
the warning firing forever, defeating the retirement.
We added `remove-empty-dir`, a new action type, rather than relaxing the
"never remove directories" posture generally:
- It calls `fs.rmdirSync` only — never `fs.rmSync`, `{ recursive: true }`, or
`{ force: true }`. A non-empty directory fails the underlying syscall and is
treated as a successful no-op (`skipped-not-empty`), not swept.
- Emptiness is re-checked immediately before the call, not trusted from
planning time, so a file that survived an earlier action in the same run (a
failed removal, or a legitimately preserved unknown file) keeps the
directory alive.
- The target must not be a symlink, and its realpath must resolve strictly
inside — and never equal — the config directory's own realpath.
- Any unexpected failure degrades to `left-in-place`, matching every sibling
action type's non-throwing posture.
**Recursive directory removal remains deliberately absent.** This primitive
only retires a directory NODE once every file inside it has already been
individually classified and actioned by other, ordinary file-level actions in
the same migration — it is not a shortcut for sweeping a subtree in one step,
and a migration author who wants that should still enumerate files
individually per migration 003's and 009's pattern.
See `docs/installer-migrations.md#action-types` (`remove-empty-dir`) and
`src/installer-migrations/009-pi-retire-reserved-hooks-dir.cts`.