Skip to content

Every migration in LifeosUpgrade.ts throws on apply(), so its documented default mode cannot succeed #1549

Description

@tzioup

LIFEOS/TOOLS/LifeosUpgrade.ts documents itself as a migration runner, but all seven registered migrations throw unconditionally when applied. The default mode described in the header can therefore only ever exit 1 at the first not-yet-applied migration.

Reproduced against a clean clone of main at 4e33cc8 — no local install involved.

Reproduce

git clone --depth 1 https://github.com/danielmiessler/LifeOS /tmp/lifeos-check
cd /tmp/lifeos-check/LifeOS/install/LIFEOS/TOOLS

# 7 migrations registered
grep -cE '^\s*id: "m-' LifeosUpgrade.ts
# → 7

# 7 apply() bodies that throw
grep -cE 'throw new Error\("m-00' LifeosUpgrade.ts
# → 7

# rollback is declared but never called anywhere in the file
grep -n 'rollback' LifeosUpgrade.ts
# → 37:  /** Optional rollback for emergencies; not run automatically. */
# → 38:  rollback?: (ctx: MigrationContext) => void;
# (no call site)

The contradiction

The file header (lines 3–7) states:

default runs all not-yet-applied migrations in registry order, fail-loud.

and documents exit code 0 as "all migrations applied or already-applied".

runMigrations() (line 259) iterates toApply and calls m.apply(ctx) inside a try/catch that exits 1 on throw (lines 268–275). Since every apply() throws, the only reachable exit-0 path is toApply.length === 0 — i.e. everything already detected as applied. If any migration is detected as not-applied, the tool cannot do anything about it; it reports and exits 1.

The individual throw messages are consistent about this and read as deliberate — several say "detect-only", "manual remediation required", or point to an interactive session (m-004: "Run the Phase G migration session interactively per PhaseG-design.md, NOT via LifeosUpgrade.ts. This migration is detect-only."). So the implementation looks intentional; it's the documented contract that doesn't match it.

Why this matters for the project

Anyone upgrading in place reads "idempotent migration runner", runs the default mode, gets a non-zero exit and the words "Migration halted. Partial state may exist." That message suggests a failed write and a possibly-inconsistent install, when in fact nothing was attempted or mutated. That's an alarming false signal on a path users hit exactly when they're already unsure whether their install is healthy.

There's also no persisted ledger — applied-state is re-derived by filesystem inspection on every run (each migration's isApplied()), so there's no record of what a previous run concluded.

Suggested fix (one concrete action)

Make the contract match the code: document and behave as a detect-only diagnostic.

  • Header: replace "default runs all not-yet-applied migrations" with a statement that all current migrations are detect-only and remediation is manual.
  • runMigrations(): when every toApply entry is detect-only, print the remediation list and exit 0 with findings (or a distinct code documented as "action required"), rather than exiting 1 with "Partial state may exist" after attempting nothing.
  • Either drop the unused rollback? field or note in the interface comment that no migration currently implements it.

Happy to open a PR for the header + runMigrations() change if that framing is right. I've deliberately not proposed implementing the seven apply() paths, since the throw messages suggest that's intentional and some of them require interactive authorization.


Related context, not duplicates: #1539 (installer hook inventory), #1541 (ReferenceCheck.ts), #1543 (nudge layer). This one is a separate subsystem with a separate fix.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions