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.
96 lines
5.7 KiB
Markdown
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`.
|